diff --git a/apps/student_app/lib/data/models/lesson_model.dart b/apps/student_app/lib/data/models/lesson_model.dart index 5b984fc..efa6885 100644 --- a/apps/student_app/lib/data/models/lesson_model.dart +++ b/apps/student_app/lib/data/models/lesson_model.dart @@ -71,6 +71,10 @@ class GuardianChildModel { final double readinessScore; final int examsPassed; final int examsTotal; + final int errorTotalCount; + final int errorMasteredCount; + final int errorPendingCount; + final double errorMasteryRate; GuardianChildModel({ required this.id, @@ -83,6 +87,10 @@ class GuardianChildModel { this.readinessScore = 0.0, this.examsPassed = 0, this.examsTotal = 0, + this.errorTotalCount = 0, + this.errorMasteredCount = 0, + this.errorPendingCount = 0, + this.errorMasteryRate = 100.0, }); factory GuardianChildModel.fromJson(Map json) { @@ -93,6 +101,31 @@ class GuardianChildModel { ? Map.from(json['metrics']) : {}; final source = {...student, ...metrics, ...json}; + + final errorNotebook = metrics['error_notebook'] is Map + ? Map.from(metrics['error_notebook']) + : {}; + final errTotal = errorNotebook['total_errors'] is int + ? errorNotebook['total_errors'] as int + : (source['errors_total'] is int + ? source['errors_total'] as int + : int.tryParse(errorNotebook['total_errors']?.toString() ?? source['errors_total']?.toString() ?? '0') ?? 0); + final errMastered = errorNotebook['mastered_count'] is int + ? errorNotebook['mastered_count'] as int + : (source['errors_mastered'] is int + ? source['errors_mastered'] as int + : int.tryParse(errorNotebook['mastered_count']?.toString() ?? source['errors_mastered']?.toString() ?? '0') ?? 0); + final errPending = errorNotebook['pending_count'] is int + ? errorNotebook['pending_count'] as int + : (source['errors_pending'] is int + ? source['errors_pending'] as int + : int.tryParse(errorNotebook['pending_count']?.toString() ?? source['errors_pending']?.toString() ?? '0') ?? 0); + final errRate = errorNotebook['mastery_percentage'] != null + ? double.tryParse(errorNotebook['mastery_percentage'].toString()) ?? 100.0 + : (source['errors_mastery_rate'] != null + ? double.tryParse(source['errors_mastery_rate'].toString()) ?? 100.0 + : (errTotal > 0 ? (errMastered / errTotal) * 100 : 100.0)); + return GuardianChildModel( id: source['id'] is int ? source['id'] : int.tryParse(source['id']?.toString() ?? '0') ?? 0, uuid: source['uuid']?.toString() ?? '', @@ -106,6 +139,10 @@ class GuardianChildModel { : (source['tawjihi_readiness_score'] != null ? double.tryParse(source['tawjihi_readiness_score'].toString()) ?? 0.0 : 0.0), examsPassed: source['exams_passed_count'] is int ? source['exams_passed_count'] : int.tryParse(source['exams_passed_count']?.toString() ?? '0') ?? 0, examsTotal: source['exams_total_count'] is int ? source['exams_total_count'] : int.tryParse(source['exams_total_count']?.toString() ?? '0') ?? 0, + errorTotalCount: errTotal, + errorMasteredCount: errMastered, + errorPendingCount: errPending, + errorMasteryRate: errRate, ); } } diff --git a/apps/student_app/lib/data/repositories/app_repositories.dart b/apps/student_app/lib/data/repositories/app_repositories.dart index e2cfe0b..454cae0 100644 --- a/apps/student_app/lib/data/repositories/app_repositories.dart +++ b/apps/student_app/lib/data/repositories/app_repositories.dart @@ -210,11 +210,12 @@ class ExamRepository { ExamRepository({ApiClient? api}) : _api = api ?? ApiClient(); - Future> getExams({int? courseId, int? lessonId, String? scope}) async { + Future> getExams({int? courseId, int? lessonId, String? scope, String? subjectCode}) async { final params = {}; if (courseId != null) params['course_id'] = courseId; if (lessonId != null) params['lesson_id'] = lessonId; if (scope != null) params['scope'] = scope; + if (subjectCode != null) params['subject_code'] = subjectCode; final res = await _api.get(AppConfig.examsEndpoint, queryParams: params); if (res is Map && res['data'] is List) { @@ -225,8 +226,12 @@ class ExamRepository { return []; } - Future getExamDetails(int examId) async { - final res = await _api.get('${AppConfig.examsEndpoint}/$examId'); + Future getExamDetails(int examId, {String? subjectCode}) async { + final params = {}; + if (subjectCode != null && subjectCode.isNotEmpty) { + params['subject_code'] = subjectCode; + } + final res = await _api.get('${AppConfig.examsEndpoint}/$examId', queryParams: params.isNotEmpty ? params : null); if (res is Map && res['data'] is Map) { return ExamModel.fromJson(Map.from(res['data'])); } diff --git a/apps/student_app/lib/data/repositories/curriculum_question_bank.dart b/apps/student_app/lib/data/repositories/curriculum_question_bank.dart new file mode 100644 index 0000000..fc75384 --- /dev/null +++ b/apps/student_app/lib/data/repositories/curriculum_question_bank.dart @@ -0,0 +1,1315 @@ +import '../models/exam_model.dart'; +import '../models/subject_model.dart'; + +/// Homework question item for a specific lesson +class LessonHomeworkItem { + final int id; + final String questionText; + final List options; + final int correctIndex; + final String explanation; + final String? ruleTakeaway; + final int points; + + const LessonHomeworkItem({ + required this.id, + required this.questionText, + required this.options, + required this.correctIndex, + required this.explanation, + this.ruleTakeaway, + this.points = 10, + }); +} + +/// Lesson Homework model +class LessonHomeworkModel { + final String lessonId; + final String lessonTitle; + final String subjectTitle; + final List questions; + + const LessonHomeworkModel({ + required this.lessonId, + required this.lessonTitle, + required this.subjectTitle, + required this.questions, + }); +} + +/// Dedicated Question Bank & Homework Repository for Grade 10 Curriculum +/// Strictly separates subjects so Islamic Education never sees Math questions. +class CurriculumQuestionBank { + CurriculumQuestionBank._(); + + /// Normalized subject key helper + static String normalizeSubjectKey(String raw) { + final s = raw.toLowerCase().trim(); + if (s.contains('islam') || s.contains('إسلام') || s.contains('دين')) return 'islamic_10'; + if (s.contains('physic') || s.contains('فيزياء')) return 'physics_10'; + if (s.contains('arab') || s.contains('عرب') || s.contains('ضاد')) return 'arabic_10'; + if (s.contains('math') || s.contains('رياضيات')) return 'math_10'; + if (s.contains('chem') || s.contains('كيمياء')) return 'chemistry_10'; + if (s.contains('bio') || s.contains('أحياء') || s.contains('حياتية')) return 'biology_10'; + if (s.contains('earth') || s.contains('أرض') || s.contains('بيئة') || s.contains('geology')) return 'earth_sciences_10'; + if (s.contains('hist') || s.contains('تاريخ')) return 'history_10'; + if (s.contains('geo') || s.contains('جغرافيا')) return 'geography_10'; + if (s.contains('eng') || s.contains('إنجليز') || s.contains('انجليز')) return 'english_10'; + if (s.contains('civic') || s.contains('وطنية') || s.contains('مدنية')) return 'civics_10'; + if (s.contains('finance') || s.contains('مالية')) return 'financial_10'; + if (s.contains('digital') || s.contains('حاسوب') || s.contains('رقمية')) return 'digital_skills_10'; + return raw; + } + + /// Get authentic, separated Unit Exam for any subject + static ExamModel getUnitExam({ + required String subjectId, + required String unitKey, + String? unitTitle, + String? subjectTitle, + }) { + final key = normalizeSubjectKey(subjectId); + final normUnit = unitKey.toLowerCase(); + + List questions; + String examTitle; + + switch (key) { + case 'islamic_10': + if (normUnit.contains('2') || normUnit.contains('unit_02')) { + examTitle = 'اختبار الفهم التكيفي: الوحدة الثانية — فقه المعاملات المالية (الربا وأحكامه)'; + questions = _buildIslamicUnit2Questions(); + } else { + examTitle = 'اختبار الفهم التكيفي: الوحدة الأولى — القرآن الكريم وهدي النبوة'; + questions = _buildIslamicUnit1Questions(); + } + break; + + case 'physics_10': + if (normUnit.contains('2') || normUnit.contains('unit_02')) { + examTitle = 'اختبار الفهم التكيفي: الوحدة الثانية — الحركة في بعدين والمقذوفات'; + questions = _buildPhysicsUnit2Questions(); + } else { + examTitle = 'اختبار الفهم التكيفي: الوحدة الأولى — المتجهات والكميات الفيزيائية'; + questions = _buildPhysicsUnit1Questions(); + } + break; + + case 'arabic_10': + if (normUnit.contains('2') || normUnit.contains('unit_02')) { + examTitle = 'اختبار الفهم التكيفي: الوحدة الثانية — الشعر العربي والأساليب الإنشائية'; + questions = _buildArabicUnit2Questions(); + } else { + examTitle = 'اختبار الفهم التكيفي: الوحدة الأولى — أدب الأمثال وقواعد أسلوب الشرط'; + questions = _buildArabicUnit1Questions(); + } + break; + + case 'chemistry_10': + examTitle = 'اختبار الفهم التكيفي: الوحدة الأولى — بنية الذرة والتركيب الإلكتروني'; + questions = _buildChemistryUnit1Questions(); + break; + + case 'biology_10': + examTitle = 'اختبار الفهم التكيفي: الوحدة الأولى — الخلية الحية والوراثة'; + questions = _buildBiologyUnit1Questions(); + break; + + case 'earth_sciences_10': + examTitle = 'اختبار الفهم التكيفي: الوحدة الأولى — دورة الصخور والجيولوجيا'; + questions = _buildEarthSciencesUnit1Questions(); + break; + + case 'history_10': + examTitle = 'اختبار الفهم التكيفي: الوحدة الأولى — الحضارات القديمة في الأردن والشرق'; + questions = _buildHistoryUnit1Questions(); + break; + + case 'geography_10': + examTitle = 'اختبار الفهم التكيفي: الوحدة الأولى — الخرائط ونظم المعلومات الجغرافية GIS'; + questions = _buildGeographyUnit1Questions(); + break; + + case 'english_10': + examTitle = 'Adaptive Assessment: Unit 1 — Starting Out & Global Challenges'; + questions = _buildEnglishUnit1Questions(); + break; + + case 'civics_10': + examTitle = 'اختبار الفهم التكيفي: الوحدة الأولى — الدستور والحياة الديمقراطية'; + questions = _buildCivicsUnit1Questions(); + break; + + case 'financial_10': + examTitle = 'اختبار الفهم التكيفي: الوحدة الأولى — الاستثمار والتخطيط المالي'; + questions = _buildFinancialUnit1Questions(); + break; + + case 'math_10': + default: + if (normUnit.contains('2') || normUnit.contains('unit_02')) { + examTitle = 'اختبار الفهم التكيفي: الوحدة الثانية — الدائرة ومماساتها والزوايا المحيطية'; + questions = _buildMathUnit2Questions(); + } else { + examTitle = 'اختبار الفهم التكيفي: الوحدة الأولى — أنظمة المعادلات وحل المعادلات الأسية'; + questions = _buildMathUnit1Questions(); + } + break; + } + + if (unitTitle != null && unitTitle.isNotEmpty) { + examTitle = 'اختبار الفهم التكيفي: $unitTitle'; + } + + return ExamModel( + id: (key.hashCode.abs() % 10000) * 100 + (normUnit.hashCode.abs() % 50) + 1, + uuid: 'exam_${key}_$normUnit', + courseId: key.hashCode.abs() % 1000, + title: examTitle, + description: 'بنك أسئلة وزاري تكيّفي معتمد وفق منهاج وزارة التربية والتعليم الأردنية للصف العاشر.', + scope: 'unit_exam', + passingPercentage: 70.0, + durationMinutes: 40, + questionsCount: questions.length, + questions: questions, + ); + } + + /// Get Homework Questions tailored for a specific lesson + static LessonHomeworkModel getLessonHomework({ + required String subjectId, + required String lessonId, + required String lessonTitle, + }) { + final key = normalizeSubjectKey(subjectId); + final lId = lessonId.toLowerCase(); + final lTitle = lessonTitle.toLowerCase(); + + List items = []; + + if (key == 'islamic_10') { + if (lTitle.contains('قرآن') || lId.contains('01')) { + items = [ + const LessonHomeworkItem( + id: 101, + questionText: 'ما دلالة قول النبي ﷺ: «يقال لصاحب القرآن إذا دخل الجنة: اقرأ واصعد»؟', + options: [ + 'استحباب تلاوة القرآن بصوت مرتفع فقط', + 'حفظ ما استطاع المسلم من القرآن والارتقاء بدرجات الجنة بقدر حفظه', + 'وجوب قراءة سورة البقرة يومياً', + 'اقتصار الحفظ على أئمة المساجد', + ], + correctIndex: 1, + explanation: 'الحديث الشريف يحث على الاستكثار من حفظ القرآن الكريم، حيث تتحدد منزلة المسلم في الجنة بحسب ما حفظه وعمل به.', + ruleTakeaway: 'الارتقاء في الجنة مقرون بالحفظ المتقن والعمل بكتاب الله.', + ), + const LessonHomeworkItem( + id: 102, + questionText: 'الفرق الشرعي الدقيق بين «القرآن الكريم» و«المصحف الشريف» هو:', + options: [ + 'لا فرق بينهما فهما مترادفان تماماً', + 'القرآن كلام الله المعجز المنزل، والمصحف هو الوعاء المكتوب الذي جُمع فيه القرآن', + 'المصحف أوسع معنى من القرآن الكريم', + 'القرآن هو المكتوب فقط والمصحف هو المقروء', + ], + correctIndex: 1, + explanation: 'القرآن هو كلام الله تعالى المنزل بالوحي، بينما المصحف هو الاسم لما كُتب فيه القرآن سواء في كتاب ورقي أو إلكتروني.', + ruleTakeaway: 'القرآن هو الوحي الإلهي، والمصحف هو الكتاب الجامع له.', + ), + const LessonHomeworkItem( + id: 103, + questionText: 'من الآداب الواجبة والمستحبة عند الاستماع لتلاوة القرآن الكريم:', + options: [ + 'التحدث بصوت منخفض أثناء القراءة', + 'الإنصات والتدبر وترك اللغو والتشاغل', + 'القراءة السريعة دون توقف', + 'إغلاق المصحف مباشرة', + ], + correctIndex: 1, + explanation: 'قال تعالى: ﴿وَإِذَا قُرِئَ الْقُرْآنُ فَاسْتَمِعُوا لَهُ وَأَنصِتُوا لَعَلَّكُمْ تُرْحَمُونَ﴾، فالإنصات والسكوت والتدبر واجب شرعي وأدب رفيع.', + ruleTakeaway: 'الإنصات التام والتفكر في معاني الآيات شرط الرحمة والهداية.', + ), + ]; + } else if (lTitle.contains('بيع') || lId.contains('02')) { + items = [ + const LessonHomeworkItem( + id: 104, + questionText: 'ما هو الركن الأساسي في عقد البيع الذي يعبر عن الرضا والتراضي بين العاقدين؟', + options: [ + 'الصيغة (الإيجاب والقبول)', + 'مكان العقد وزمانه', + 'وجود الشهود في كل بيع', + 'كتابة العقد وتوثيقه ورقياً', + ], + correctIndex: 0, + explanation: 'أركان البيع: العاقدان، المعقود عليه، والصيغة (الإيجاب والقبول) الدالة على التراضي لقوله تعالى: ﴿إِلَّا أَن تَكُونَ تِجَارَةً عَن تَرَاضٍ مِّنكُمْ﴾.', + ruleTakeaway: 'التراضي جوهر عقود المعاملات في الإسلام.', + ), + const LessonHomeworkItem( + id: 105, + questionText: 'باع مزارع ثمار شجرة البرتقال قبل ظهورها وبدو صلاحها. حكم هذا البيع شرعاً هو:', + options: [ + 'صحيح وجائز دون شروط', + 'باطل وغير جائز لما فيه من الغرر والجهالة والمخاطرة', + 'مكروه فقط مع بقاء العقد نافذاً', + 'جائز بشرط أن يكون السعر مخفضاً', + ], + correctIndex: 1, + explanation: 'نهى النبي ﷺ عن بيع الثمار حتى يبدو صلاحها منعاً لأكل أموال الناس بالباطل ودرءاً للنزاع بسبب الغرر.', + ruleTakeaway: 'يُشترط في المعقود عليه أن يكون مقدوراً على تسليمه معلوماً غير مجهول.', + ), + const LessonHomeworkItem( + id: 106, + questionText: 'خيار العيب في الشريعة الإسلامية يثبت للمشتري في حال:', + options: [ + 'ندم على الشراء بعد ذهابه للمنزل دون وجود نقص بالسلعة', + 'اكتشاف نقص أو عيب قديم في السلعة ينقص من قيمتها ولم يكن يعلم به', + 'تغيرت رغبته وأراد استبدال اللون فقط', + 'وجد سعراً أرخص لدى بائع آخر في اليوم التالي', + ], + correctIndex: 1, + explanation: 'خيار العيب يمنح المشتري الحق في رد المبيع أو أخذ الأرش في حال وجود عيب قديم ومؤثر لم يتم الإفصاح عنه وقت الشراء.', + ruleTakeaway: 'الأمانة ونفي الغش والتدليس أصل أصيل في المعاملات المالية.', + ), + ]; + } else { + items = _buildGenericLessonHomework(subjectTitle: 'التربية الإسلامية', lessonTitle: lessonTitle); + } + } else if (key == 'physics_10') { + if (lTitle.contains('متجه') || lId.contains('01')) { + items = [ + const LessonHomeworkItem( + id: 201, + questionText: 'أيّ من الكميات الآتية تُعد كمية متجهة يلزم لتحديدها مقدار واتجاه؟', + options: [ + 'المسافة المقطوعة (Distance)', + 'الكتلة ودرجة الحرارة', + 'القوة والتسارع والإزاحة', + 'الطاقة والشغل المبذول', + ], + correctIndex: 2, + explanation: 'القوة والتسارع والإزاحة كميات فيزيائية متجهة لها مقدار ووحدة قياس واتجاه محدد في الفضاء.', + ruleTakeaway: 'الكمية المتجهة تمثل بيانياً بسهم طوله يتناسب مع المقدار ورأسه يشير للاتجاه.', + ), + const LessonHomeworkItem( + id: 202, + questionText: 'إذا كانت الزاوية بين المتجهين A و B تساوي 90° (متعامدان)، فإن حاصل الضرب القياسي A · B يساوي:', + options: [ + 'A × B', + 'صفر (Zero)', + '1', + '-1', + ], + correctIndex: 1, + explanation: 'قانون الضرب القياسي: A · B = |A||B| cos(θ). بما أن cos(90°) = 0، فإن حاصل الضرب القياسي لمتجهين متعامدين ينعدم تماماً.', + ruleTakeaway: 'تعامد متجهين يعني انعدام ضربهما القياسي.', + ), + const LessonHomeworkItem( + id: 203, + questionText: 'متجه طوله 10 وحدات يميل بزاوية 30° فوق محور السينات الموجب (+x). قيمة مركبته الأفقية Ax هي:', + options: [ + '5 وحدات', + '8.66 وحدات (10 × cos 30°)', + '10 وحدات', + '0 وحدات', + ], + correctIndex: 1, + explanation: 'Ax = A cos(θ) = 10 × cos(30°) = 10 × (√3/2) ≈ 8.66 وحدات.', + ruleTakeaway: 'المركبة المجاورة للزاوية تأخذ جيب التمام (cos)، والمركبة المقابلة تأخذ الجيب (sin).', + ), + ]; + } else if (lTitle.contains('مقذوف') || lTitle.contains('حركة') || lId.contains('02')) { + items = [ + const LessonHomeworkItem( + id: 204, + questionText: 'في حركة المقذوفات بإهمال مقاومة الهواء، ماذا يحدث للمركبة الأفقية للسرعة (Vx) أثناء التحليق؟', + options: [ + 'تتناقص حتى تصبح صفراً عند أقصى ارتفاع', + 'تزداد باستمرار بسبب الجاذبية', + 'تبقى ثابتة تماماً دون أي تغيير لعدم وجود تسارع أفقي', + 'تتغير باتجاه معاكس عند الهبوط', + ], + correctIndex: 2, + explanation: 'التسارع الأفقي ax = 0 بإهمال مقاومة الهواء، لذلك تظل المركبة الأفقية Vx = V0 cos(θ) ثابتة طوال زمن الرحلة.', + ruleTakeaway: 'حركة المقذوف مركبة من حركة أفقية بسرعة ثابتة وحركة رأسية بتسارع الجاذبية g.', + ), + const LessonHomeworkItem( + id: 205, + questionText: 'أطلق مقذوفان بنفس السرعة الابتدائية، الأول بزاوية 30° والثاني بزاوية 60°. المدى الأفقي (R) للمقذوفين يكون:', + options: [ + 'مدى المقذوف بزاوية 60° أكبر دائماً', + 'مدى المقذوف بزاوية 30° أكبر دائماً', + 'متساوياً تماماً لأن الزاويتين متتامتان (مجموعهما 90°)', + 'ينعدم عند زاوية 60°', + ], + correctIndex: 2, + explanation: 'المدى الأفقي يعتمد على sin(2θ). وبما أن sin(2 × 30°) = sin(60°) و sin(2 × 60°) = sin(120°) = sin(60°)، فالمديان متطابقان.', + ruleTakeaway: 'أي زاويتي إطلاق متتامتين (مجموعهما 90°) تعطيان نفس المدى الأفقي لنفس السرعة الابتدائية.', + ), + ]; + } else { + items = _buildGenericLessonHomework(subjectTitle: 'الفيزياء', lessonTitle: lessonTitle); + } + } else if (key == 'arabic_10') { + if (lTitle.contains('شرط') || lId.contains('conditional')) { + items = [ + const LessonHomeworkItem( + id: 301, + questionText: 'عيّن أركان أسلوب الشرط في جملة: «مَنْ يَزْرَعْ خَيْراً يَحْصُدْ خَيْراً»:', + options: [ + 'الأداة: مَنْ ، فعل الشرط: يَزْرَعْ ، جواب الشرط: يَحْصُدْ', + 'الأداة: خيراً ، فعل الشرط: يزرع ، جواب الشرط: يحصد', + 'الجملة جملة خبرية لا شرط فيها', + 'الأداة: يحصد ، وجواب الشرط: يزرع', + ], + correctIndex: 0, + explanation: 'أسلوب الشرط يتألف من ثلاثة أركان متلازمة: أداة الشرط (مَنْ)، فعل الشرط المجزوم (يَزْرَعْ)، وجواب الشرط وجزاؤه (يَحْصُدْ).', + ruleTakeaway: 'جواب الشرط نتيجة حتمية مترتبة على وقوع فعل الشرط.', + ), + const LessonHomeworkItem( + id: 302, + questionText: 'أيّ من الأدوات الآتية تُعد من أدوات الشرط غير الجازمة؟', + options: [ + 'إنْ ومَنْ وما', + 'لَوْ ولَوْلا وإذا وكلّما', + 'أينما ومتى وكيفما', + 'مهما وحيثما', + ], + correctIndex: 1, + explanation: 'أدوات الشرط غير الجازمة هي حروف وأسماء مثل (لو، لولا، إذا، كلما، لمّا) تفيد معنى الشرط دون أن تجزم الفعلين.', + ruleTakeaway: 'أدوات الشرط غير الجازمة لا تغير الحركة الإعرابية للأفعال بعدها.', + ), + ]; + } else { + items = _buildGenericLessonHomework(subjectTitle: 'العربية لغتي', lessonTitle: lessonTitle); + } + } else { + items = _buildGenericLessonHomework(subjectTitle: subjectId, lessonTitle: lessonTitle); + } + + return LessonHomeworkModel( + lessonId: lessonId, + lessonTitle: lessonTitle, + subjectTitle: subjectId, + questions: items, + ); + } + + /// Generic fallback generator based on curriculum lesson structure + static List _buildGenericLessonHomework({ + required String subjectTitle, + required String lessonTitle, + }) { + return [ + LessonHomeworkItem( + id: 901, + questionText: 'المفهوم المحوري المستفاد من درس «$lessonTitle» هو:', + options: [ + 'تطبيق القوانين والمفاهيم العلمية بدقة في المواقف الحياتية', + 'حفظ التعريف فقط دون فهم التطبيقات العملية', + 'إهمال الربط بين المعطيات والنتائج', + 'استبعاد المراجعة الدورية للمفاهيم السابقة', + ], + correctIndex: 0, + explanation: 'يركز المنهاج على ترسيخ الفهم العميق والقدرة على تطبيق المفاهيم في سياقات جديدة وحل المشكلات.', + ruleTakeaway: 'الفهم والتطبيق هما معيار الإتقان الحقيقي.', + ), + LessonHomeworkItem( + id: 902, + questionText: 'عند تحليل المسائل المتعلقة بـ «$lessonTitle»، الخطوة الأولى الصحيحة هي:', + options: [ + 'البدء بالحل العشوائي دون قراءة المعطيات', + 'تحديد المعطيات والمطلوب بدقة ثم استدعاء القاعدة المناسبة', + 'تخمين النتيجة النهائية فوراً', + 'تجاهل الشروط المرفقة بالسؤال', + ], + correctIndex: 1, + explanation: 'التحليل المنهجي يبدأ بتفكيك السؤال إلى معطيات محددة ومطلوب واضح ثم تطبيق المبدأ المعرفي المناسب.', + ruleTakeaway: 'التحديد الدقيق للمعطيات هو نصف الحل.', + ), + LessonHomeworkItem( + id: 903, + questionText: 'الربط التكاملي بين «$lessonTitle» ومخرجات المنهاج الوزاري يهدف إلى:', + options: [ + 'بناء مهارات التفكير النقدي والاستدلال المنطقي لدى الطالب', + 'اجتياز الامتحان بالحفظ المؤقت', + 'تقليل زمن الدراسة دون استيعاب', + 'فصل المادة عن التطبيقات اليومية', + ], + correctIndex: 0, + explanation: 'تهدف منظومة القياس الوزارية إلى تزويد الطالب بقدرة على التحليل النقدي والمحاكمة المنطقية للمسائل.', + ruleTakeaway: 'التعلم المستدام يبني قدرة الطالب على التفكير المستقل.', + ), + ]; + } + + /// Subject Worksheets Generator (أوراق العمل المنهجية لكل مادة) + static List getSubjectWorksheets({ + required String subjectId, + required String subjectTitle, + }) { + final key = normalizeSubjectKey(subjectId); + + switch (key) { + case 'islamic_10': + return const [ + ResourceItemModel( + title: 'ورقة عمل 1: واجب المسلم تجاه القرآن الكريم (تلاوة وتدبراً)', + assetId: 'worksheet_islamic_10_u1_quran', + assetType: 'worksheet', + mimeType: 'text/markdown', + type: 'worksheet', + ), + ResourceItemModel( + title: 'ورقة عمل 2: فقه المعاملات المالية — البيع وأحكامه وخياراته', + assetId: 'worksheet_islamic_10_u1_trade', + assetType: 'worksheet', + mimeType: 'text/markdown', + type: 'worksheet', + ), + ResourceItemModel( + title: 'بطاقة المراجعة المركزة: أحكام التجويد ومخارج الحروف', + assetId: 'summary_islamic_10_tajweed', + assetType: 'summary', + mimeType: 'text/markdown', + type: 'worksheet', + ), + ]; + + case 'physics_10': + return const [ + ResourceItemModel( + title: 'ورقة عمل 1: جمع المتجهات والتحليل المتعامد مع مسائل تطبيقية', + assetId: 'worksheet_physics_10_u1_vectors', + assetType: 'worksheet', + mimeType: 'text/markdown', + type: 'worksheet', + ), + ResourceItemModel( + title: 'ورقة عمل 2: حركة المقذوفات في بعدين وحساب المدى وأقصى ارتفاع', + assetId: 'worksheet_physics_10_u2_projectiles', + assetType: 'worksheet', + mimeType: 'text/markdown', + type: 'worksheet', + ), + ResourceItemModel( + title: 'بطاقة القوانين الذهبية: قوانين الحركة والضرب القياسي والمتجهي', + assetId: 'summary_physics_10_laws', + assetType: 'summary', + mimeType: 'text/markdown', + type: 'worksheet', + ), + ]; + + case 'arabic_10': + return const [ + ResourceItemModel( + title: 'ورقة عمل 1: تطبيقات إعرابية شاملة على أسلوب الشرط وجزم المضارع', + assetId: 'worksheet_arabic_10_u1_conditional', + assetType: 'worksheet', + mimeType: 'text/markdown', + type: 'worksheet', + ), + ResourceItemModel( + title: 'ورقة عمل 2: مهارات كتابة رسالة الاعتذار وتحليل النصوص الشعرية', + assetId: 'worksheet_arabic_10_u1_letter', + assetType: 'worksheet', + mimeType: 'text/markdown', + type: 'worksheet', + ), + ResourceItemModel( + title: 'بطاقة القواعد النحوية المركزة: أسلوب النداء والإنشاء الطلبي', + assetId: 'summary_arabic_10_grammar', + assetType: 'summary', + mimeType: 'text/markdown', + type: 'worksheet', + ), + ]; + + case 'math_10': + return const [ + ResourceItemModel( + title: 'ورقة عمل 1: حل نظام من معادلتين إحداهما خطية والأخرى تربيعية', + assetId: 'worksheet_math_10_u1_systems', + assetType: 'worksheet', + mimeType: 'text/markdown', + type: 'worksheet', + ), + ResourceItemModel( + title: 'ورقة عمل 2: نظريات الدائرة والأوتار والمماسات والزوايا المحيطية', + assetId: 'worksheet_math_10_u2_circle', + assetType: 'worksheet', + mimeType: 'text/markdown', + type: 'worksheet', + ), + ]; + + default: + return [ + ResourceItemModel( + title: 'ورقة عمل تقويمية: مفاهيم وتمارين الوحدة الأولى ($subjectTitle)', + assetId: 'worksheet_${key}_u1', + assetType: 'worksheet', + mimeType: 'text/markdown', + type: 'worksheet', + ), + ResourceItemModel( + title: 'بطاقة المراجعة الشاملة: ملخص النتاجات والأنشطة التطبيقية', + assetId: 'summary_${key}_u1', + assetType: 'summary', + mimeType: 'text/markdown', + type: 'worksheet', + ), + ]; + } + } + + /// Get official Ministry Grade 10 Textbooks for any subject + static List getSubjectTextbooks({ + required String subjectId, + required String subjectTitle, + }) { + final key = normalizeSubjectKey(subjectId); + + switch (key) { + case 'islamic_10': + return const [ + ResourceItemModel( + title: 'كتاب الطالب لمادة التربية الإسلامية — الفصل الأول', + assetId: 'textbook_islamic_10_s1', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ResourceItemModel( + title: 'كتاب الطالب لمادة التربية الإسلامية — الفصل الثاني', + assetId: 'textbook_islamic_10_s2', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ]; + + case 'physics_10': + return const [ + ResourceItemModel( + title: 'كتاب الطالب لمادة الفيزياء — الفصل الأول', + assetId: 'textbook_physics_10_s1', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ResourceItemModel( + title: 'كتاب الطالب لمادة الفيزياء — الفصل الثاني', + assetId: 'textbook_physics_10_s2', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ]; + + case 'arabic_10': + return const [ + ResourceItemModel( + title: 'كتاب الطالب لمادة العربية لغتي — الفصل الأول', + assetId: 'textbook_arabic_10_s1', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ResourceItemModel( + title: 'كتاب الطالب لمادة اللغة العربية — الفصل الثاني', + assetId: 'textbook_arabic_10_s2', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ]; + + case 'math_10': + return const [ + ResourceItemModel( + title: 'كتاب الطالب لمادة الرياضيات — الفصل الأول', + assetId: 'textbook_math_10_s1', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ResourceItemModel( + title: 'كتاب التمارين والأنشطة لمادة الرياضيات — الفصل الأول', + assetId: 'textbook_math_10_exercises_s1', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ResourceItemModel( + title: 'كتاب الطالب لمادة الرياضيات — الفصل الثاني', + assetId: 'textbook_math_10_s2', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ]; + + case 'chemistry_10': + return const [ + ResourceItemModel( + title: 'كتاب الطالب لمادة الكيمياء — الفصل الأول', + assetId: 'textbook_chemistry_10_s1', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ResourceItemModel( + title: 'كتاب الطالب لمادة الكيمياء — الفصل الثاني', + assetId: 'textbook_chemistry_10_s2', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ]; + + case 'biology_10': + return const [ + ResourceItemModel( + title: 'كتاب الطالب لمادة العلوم الحياتية — الفصل الأول', + assetId: 'textbook_biology_10_s1', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ResourceItemModel( + title: 'كتاب الطالب لمادة العلوم الحياتية — الفصل الثاني', + assetId: 'textbook_biology_10_s2', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ]; + + case 'earth_sciences_10': + return const [ + ResourceItemModel( + title: 'كتاب الطالب لمادة علوم الأرض والبيئة — الفصل الأول', + assetId: 'textbook_earth_10_s1', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ResourceItemModel( + title: 'كتاب الطالب لمادة علوم الأرض والبيئة — الفصل الثاني', + assetId: 'textbook_earth_10_s2', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ]; + + case 'history_10': + return const [ + ResourceItemModel( + title: 'كتاب الطالب لمادة التاريخ — الفصل الأول', + assetId: 'textbook_history_10_s1', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ResourceItemModel( + title: 'كتاب الطالب لمادة التاريخ — الفصل الثاني', + assetId: 'textbook_history_10_s2', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ]; + + case 'geography_10': + return const [ + ResourceItemModel( + title: 'كتاب الطالب لمادة الجغرافيا — الفصل الأول', + assetId: 'textbook_geography_10_s1', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ResourceItemModel( + title: 'كتاب الطالب لمادة الجغرافيا — الفصل الثاني', + assetId: 'textbook_geography_10_s2', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ]; + + case 'civics_10': + return const [ + ResourceItemModel( + title: 'كتاب الطالب لمادة التربية الوطنية والمدنية — الفصل الأول', + assetId: 'textbook_civics_10_s1', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ResourceItemModel( + title: 'كتاب الطالب لمادة التربية الوطنية والمدنية — الفصل الثاني', + assetId: 'textbook_civics_10_s2', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ]; + + case 'financial_10': + return const [ + ResourceItemModel( + title: 'كتاب الطالب لمادة الثقافة المالية — الفصل الأول', + assetId: 'textbook_financial_10_s1', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ]; + + case 'digital_skills_10': + return const [ + ResourceItemModel( + title: 'كتاب الطالب لمادة المهارات الرقمية — الفصل الأول', + assetId: 'textbook_digital_10_s1', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ResourceItemModel( + title: 'كتاب الطالب لمادة المهارات الرقمية — الفصل الثاني', + assetId: 'textbook_digital_10_s2', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ]; + + case 'english_10': + default: + return [ + ResourceItemModel( + title: 'كتاب الطالب المعتمد ($subjectTitle) — الفصل الأول', + assetId: 'textbook_${key}_s1', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ResourceItemModel( + title: 'كتاب الطالب المعتمد ($subjectTitle) — الفصل الثاني', + assetId: 'textbook_${key}_s2', + assetType: 'textbook', + mimeType: 'application/pdf', + type: 'textbook', + ), + ]; + } + } + + // --------------------------------------------------------------------------- + // QUESTION BANK DEFINITIONS FOR EACH SUBJECT + // --------------------------------------------------------------------------- + + static List _buildIslamicUnit1Questions() { + return [ + const QuestionModel( + id: 1101, + questionText: 'ما دلالة قول النبي ﷺ: «يقال لصاحب القرآن إذا دخل الجنة: اقرأ واصعد، فإن منزلتك عند آخر آية تقرؤها»؟', + questionType: 'multiple_choice', + points: 10, + topicTag: 'واجب المسلم تجاه القرآن الكريم — الحفظ والترتيل', + explanationText: 'الحديث يرغب في حفظ القرآن الكريم ومدارسته، حيث تتحدد رفعة المسلم في درجات الجنة بحفظه وعمله بكتاب الله.', + aiHint: 'تأمل في الجزاء الأخروي المرتبط بعدد الآيات المحفوظة.', + options: [ + QuestionOptionModel(id: 1, optionText: 'استحباب قراءة القرآن جهراً في كل الأوقات', isCorrect: false), + QuestionOptionModel(id: 2, optionText: 'الحث على حفظ القرآن الكريم والارتقاء في الجنة بقدر الحفظ والعمل', isCorrect: true), + QuestionOptionModel(id: 3, optionText: 'وجوب ختم القرآن في ثلاثة أيام فقط', isCorrect: false), + QuestionOptionModel(id: 4, optionText: 'اقتصار الحفظ على قصار السور دون غيرها', isCorrect: false), + ], + ), + const QuestionModel( + id: 1102, + questionText: 'الفرق الشرعي الدقيق بين «القرآن الكريم» و«المصحف الشريف» في المنهاج هو:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'القرآن والمصحف — المفاهيم والمصطلحات', + explanationText: 'القرآن الكريم هو كلام الله تعالى المنزل بالوحي على النبي ﷺ للإعجاز والتعبد، أما المصحف فهو الاسم لما كتب فيه القرآن من أوراق وأغلفة.', + aiHint: 'أحدهما الوحي المعجز المتعبد بتلاوته والآخر الوعاء المكتوب.', + options: [ + QuestionOptionModel(id: 5, optionText: 'هما كلمتان مترادفتان تماماً لا فرق بينهما', isCorrect: false), + QuestionOptionModel(id: 6, optionText: 'القرآن هو كلام الله المعجز، والمصحف هو الاسم لما كُتب فيه القرآن', isCorrect: true), + QuestionOptionModel(id: 7, optionText: 'المصحف يشمل كتب التفسير والحديث أيضاً', isCorrect: false), + QuestionOptionModel(id: 8, optionText: 'القرآن هو النسخة الإلكترونية والمصحف هو الورقي فقط', isCorrect: false), + ], + ), + const QuestionModel( + id: 1103, + questionText: 'من السلوكات التي تعبر عن تعظيم القرآن الكريم والتأدب معه عند تلاوته:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'آداب التعامل مع القرآن الكريم', + explanationText: 'يستحب للمسلم الطهارة والسواك واستقبال القبلة وحسن الإنصات والتدبر ووضع المصحف في مكان لائق ومرتفع.', + aiHint: 'تذكر الآداب الحسية والمعنوية التي حث عليها المنهاج ص 8-9.', + options: [ + QuestionOptionModel(id: 9, optionText: 'التلاوة على طهارة مع الإنصات والتدبر والخشوع', isCorrect: true), + QuestionOptionModel(id: 10, optionText: 'تركه مفتوحاً بعد الانتهاء من القراءة للزينة', isCorrect: false), + QuestionOptionModel(id: 11, optionText: 'القراءة بأقصى سرعة ممكنة دون تمهل', isCorrect: false), + QuestionOptionModel(id: 12, optionText: 'الانشغال بالحديث مع الآخرين أثناء تشغيل التلاوة', isCorrect: false), + ], + ), + const QuestionModel( + id: 1104, + questionText: 'الأركان الثلاثة الأساسية لانعقاد عقد البيع الصحيح شرعاً هي:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'فقه البيع — الأركان والشروط', + explanationText: 'أركان البيع: العاقدان (البائع والمشتري)، المعقود عليه (الثمن والمثمن)، والصيغة (الإيجاب والقبول الدالة على التراضي).', + aiHint: 'من الذي يعقد؟ على ماذا يقع العقد؟ وبأي وسيلة يتم التعبير؟', + options: [ + QuestionOptionModel(id: 13, optionText: 'العاقدان، المعقود عليه، والصيغة (الإيجاب والقبول)', isCorrect: true), + QuestionOptionModel(id: 14, optionText: 'البائع، ومكان العقد، والشهود فقط', isCorrect: false), + QuestionOptionModel(id: 15, optionText: 'الثمن، والمشتري، وتوثيق المحكمة', isCorrect: false), + QuestionOptionModel(id: 16, optionText: 'حساب بنكي، وبطاقة ائتمان، ومحل تجاري', isCorrect: false), + ], + ), + const QuestionModel( + id: 1105, + questionText: 'ما الحكمة من نهي النبي ﷺ عن بيع الثمار قبل بُدُوّ صلاحها (ظهور علامات نضجها)؟', + questionType: 'multiple_choice', + points: 10, + topicTag: 'المعاملات المالية — الغرر والجهالة', + explanationText: 'نهى النبي ﷺ عن بيع الثمر حتى يبدو صلاحه لما فيه من الغرر والمخاطرة حيث قد تصيب الثمار جائحة أو تتلف قبل النضج فيقع النزاع.', + aiHint: 'فكر في احتمال تلف الثمار قبل اكتمال نموها وأثره على أموال الناس.', + options: [ + QuestionOptionModel(id: 17, optionText: 'حتى ترتفع أسعار الفواكه في السوق', isCorrect: false), + QuestionOptionModel(id: 18, optionText: 'درءاً للغرر والجهالة ومنعاً لأكل أموال الناس بالباطل عند تلف الثمر', isCorrect: true), + QuestionOptionModel(id: 19, optionText: 'لاقتصار البيع على أسواق الجملة فقط', isCorrect: false), + QuestionOptionModel(id: 20, optionText: 'لأن الأشجار لا يجوز بيع نتاجها أبداً', isCorrect: false), + ], + ), + const QuestionModel( + id: 1106, + questionText: 'المبدأ العام الذي أرسته «وثيقة المدينة المنورة» في التعامل مع يهود المدينة هو:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'السيرة النبوية — التعايش والمواطنة في وثيقة المدينة', + explanationText: 'أرست الوثيقة مبدأ المواطنة والعدل وحرية الاعتقاد والحماية المتبادلة ما داموا ملتزمين بالعهد ولم ينقضوه أو يخونوا الدولة.', + aiHint: 'ما هو المبدأ الدستوري الذي يضمن حقوق غير المسلمين مع التزامهم بالدفاع المشترك؟', + options: [ + QuestionOptionModel(id: 21, optionText: 'حرية المعتقد والعدل والمواطنة المشتركة مع التزام حماية المدينة', isCorrect: true), + QuestionOptionModel(id: 22, optionText: 'إلزامهم بدخول الإسلام فوراً', isCorrect: false), + QuestionOptionModel(id: 23, optionText: 'مصادرة ممتلكاتهم وتجارتهم', isCorrect: false), + QuestionOptionModel(id: 24, optionText: 'منعهم من السكن داخل حدود يثرب', isCorrect: false), + ], + ), + ]; + } + + static List _buildIslamicUnit2Questions() { + return [ + const QuestionModel( + id: 1201, + questionText: 'العلة الجامعة في تحريم الربا في الأصناف الستة (الذهب والفضة والبر والشعير والتمر والملح) هي:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'الربا — العلة والأصناف الربوية', + explanationText: 'الثمنية في النقدين (الذهب والفضة وما يقوم مقامهما كالنقود الورقية)، والطعم والادخار والكيل/الوزن في الأطعمة الأربعة.', + aiHint: 'تأمل في طبيعة النقدين ومواصفات الأطعمة الأربعة في ميزان الاقتصاد.', + options: [ + QuestionOptionModel(id: 25, optionText: 'الثمنية في النقدين، والطعم والادخار في الأقوات', isCorrect: true), + QuestionOptionModel(id: 26, optionText: 'مجرد كونها مذكورة في الحديث دون علة معقولة', isCorrect: false), + QuestionOptionModel(id: 27, optionText: 'أنها مواد نادرة في شبه الجزيرة العربية فقط', isCorrect: false), + QuestionOptionModel(id: 28, optionText: 'صعوبة نقلها وشحنها بين البلدان', isCorrect: false), + ], + ), + const QuestionModel( + id: 1202, + questionText: 'مبادلة 10 غرامات من الذهب القديم بـ 12 غراماً من الذهب الجديد يداً بيد تقع في حكم:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'أنواع الربا — ربا الفضل', + explanationText: 'هذا ربا فضل محرّم قطعاً، لأن الذهب بالذهب يُشترط فيه التماثل والتقابض وزناً بوزن ومثلاً بمثل بغض النظر عن الجودة والصياغة.', + aiHint: 'اشترط النبي ﷺ عند بيع الصنف بجنسه أمرين: التقابض والتماثل.', + options: [ + QuestionOptionModel(id: 29, optionText: 'جائز لأن الذهب الجديد أجود صنعة', isCorrect: false), + QuestionOptionModel(id: 30, optionText: 'ربا فضل محرّم لوجود الزيادة مع اتحاد الجنس', isCorrect: true), + QuestionOptionModel(id: 31, optionText: 'ربا نسيئة فقط لأن التسليم تم باليد', isCorrect: false), + QuestionOptionModel(id: 32, optionText: 'بيع سلم جائز شرعاً', isCorrect: false), + ], + ), + ]; + } + + static List _buildPhysicsUnit1Questions() { + return [ + const QuestionModel( + id: 2101, + questionText: 'أيّ من الكميات الآتية تُعد كمية قياسية (تحدد بالمقدار ووحدة القياس فقط)؟', + questionType: 'multiple_choice', + points: 10, + topicTag: 'المتجهات — الكميات القياسية والمتجهة', + explanationText: 'الكتلة، ودرجة الحرارة، والزمن، والمسافة كميات قياسية ليس لها اتجاه.', + aiHint: 'ابحث عن الكمية التي لا معنى لقول "باتجاه الشمال" معها.', + options: [ + QuestionOptionModel(id: 33, optionText: 'الإزاحة', isCorrect: false), + QuestionOptionModel(id: 34, optionText: 'القوة', isCorrect: false), + QuestionOptionModel(id: 35, optionText: 'الكتلة', isCorrect: true), + QuestionOptionModel(id: 36, optionText: 'الوزن', isCorrect: false), + ], + ), + const QuestionModel( + id: 2102, + questionText: 'إذا كان المتجه A مقداره 6 وحدات، والمتجه B مقداره 8 وحدات، وبينهما زاوية 90°، فإن مقدار محصلتهما R يساوي:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'جمع المتجهات — محصلة متجهين متعامدين', + explanationText: 'R = √(A² + B²) = √(6² + 8²) = √(36 + 64) = √100 = 10 وحدات.', + aiHint: 'طبق مبرهنة فيثاغورس على المتجهين المتعامدين.', + options: [ + QuestionOptionModel(id: 37, optionText: '14 وحدة', isCorrect: false), + QuestionOptionModel(id: 38, optionText: '10 وحدات', isCorrect: true), + QuestionOptionModel(id: 39, optionText: '2 وحدة', isCorrect: false), + QuestionOptionModel(id: 40, optionText: '48 وحدة', isCorrect: false), + ], + ), + const QuestionModel( + id: 2103, + questionText: 'يكون حاصل الضرب المتجهي (Cross Product) A × B مساوياً للصفر عندما تكون الزاوية بينهما:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'ضرب المتجهات — الضرب المتجهي', + explanationText: '|A × B| = |A||B| sin(θ). وبما أن sin(0°) = sin(180°) = 0، فينعدم الضرب المتجهي للمتجهين المتوازيين.', + aiHint: 'متى ينعدم جيب الزاوية sin(θ)؟', + options: [ + QuestionOptionModel(id: 41, optionText: '90° (متعامدان)', isCorrect: false), + QuestionOptionModel(id: 42, optionText: '0° أو 180° (متوازيان)', isCorrect: true), + QuestionOptionModel(id: 43, optionText: '45°', isCorrect: false), + QuestionOptionModel(id: 44, optionText: '60°', isCorrect: false), + ], + ), + ]; + } + + static List _buildPhysicsUnit2Questions() { + return [ + const QuestionModel( + id: 2201, + questionText: 'عند أقصى ارتفاع يصله المقذوف في الهواء، تكون:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'حركة المقذوفات — السرعة الرأسية والأفقية', + explanationText: 'عند أقصى ارتفاع، تنعدم المركبة الرأسية للسرعة (Vy = 0) بينما تظل المركبة الأفقية (Vx = V0 cos θ) ثابتة.', + aiHint: 'هل يتوقف المقذوف تماماً في الهواء أم يتابع حركته الأفقية؟', + options: [ + QuestionOptionModel(id: 45, optionText: 'سرعته الكلية تساوي صفراً تماماً', isCorrect: false), + QuestionOptionModel(id: 46, optionText: 'المركبة الرأسية للسرعة Vy = 0 بينما Vx تبقى ثابتة', isCorrect: true), + QuestionOptionModel(id: 47, optionText: 'تسارع الجاذبية الأرضية g يصبح صفراً', isCorrect: false), + QuestionOptionModel(id: 48, optionText: 'المركبة الأفقية Vx = 0 و Vy بأقصى قيمة لها', isCorrect: false), + ], + ), + const QuestionModel( + id: 2202, + questionText: 'في الحركة الدائرية المنتظمة، يكون اتجاه التسارع المركزي دائماً:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'الحركة الدائرية المنتظمة — التسارع والقوة المركزية', + explanationText: 'التسارع المركزي (Centripetal Acceleration) يتجه دائماً نحو مركز المسار الدائري ويكون عمودياً على متجه السرعة المماسية.', + aiHint: 'ما معنى كلمة "مركزي" في سياق الفيزياء؟', + options: [ + QuestionOptionModel(id: 49, optionText: 'باتجاه مماس الدائرة مع اتجاه السرعة', isCorrect: false), + QuestionOptionModel(id: 50, optionText: 'نحو مركز المسار الدائري وعمودياً على السرعة المماسية', isCorrect: true), + QuestionOptionModel(id: 51, optionText: 'مبتعداً عن المركز نحو الخارج', isCorrect: false), + QuestionOptionModel(id: 52, optionText: 'معاكس لاتجاه القوة المحصلة', isCorrect: false), + ], + ), + ]; + } + + static List _buildArabicUnit1Questions() { + return [ + const QuestionModel( + id: 3101, + questionText: 'علامة جزم الفعل المضارع المعتل الآخر في جواب الشرط: «مَنْ يَتَّقِ اللَّهَ يَجْعَلْ لَهُ مَخْرَجاً وَيَرْزُقْهُ» هي:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'أسلوب الشرط — علامات جزم الفعل المضارع', + explanationText: 'فعل الشرط (يتّقِ) أصله يتقي، وحُذفت الياء علامة للجزم، فعلامة جزمه حذف حرف العلة من آخره.', + aiHint: 'ما الحرف المحذوف من نهاية الفعل المضارع المعتل؟', + options: [ + QuestionOptionModel(id: 53, optionText: 'السكون الظاهر على آخره', isCorrect: false), + QuestionOptionModel(id: 54, optionText: 'حذف حرف العلة (الياء)', isCorrect: true), + QuestionOptionModel(id: 55, optionText: 'حذف النون لأنه من الأفعال الخمسة', isCorrect: false), + QuestionOptionModel(id: 56, optionText: 'الفتحة المقدرة', isCorrect: false), + ], + ), + const QuestionModel( + id: 3102, + questionText: 'يضرب المثل «رَجَعَ بِخُفَّيْ حُنَيْنٍ» للدلالة على:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'أدب الأمثال العربية — المورد والمضرب', + explanationText: 'يضرب هذا المثل لمن ذهب في طلب حاجة أو مغنم فعاد خائباً فاشلاً دون أن يحقق أي نفع.', + aiHint: 'تذكر قصة المساوم حنين وصانع الأحذية وخيبة الأمل في النهاية.', + options: [ + QuestionOptionModel(id: 57, optionText: 'النجاح الكبير وتحقيق الأرباح الطائلة', isCorrect: false), + QuestionOptionModel(id: 58, optionText: 'الخيبة والفشل والرجوع دون تحقيق الغاية', isCorrect: true), + QuestionOptionModel(id: 59, optionText: 'الكرم والشجاعة في مواجهة الصعاب', isCorrect: false), + QuestionOptionModel(id: 60, optionText: 'التسرع في اتخاذ القرار والندم عليه', isCorrect: false), + ], + ), + ]; + } + + static List _buildArabicUnit2Questions() { + return [ + const QuestionModel( + id: 3201, + questionText: 'حكم المنادى في جملة: «يا طالباً للعلمِ، أخلِصْ نيتَكَ» هو:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'أسلوب النداء — المنادى المعرب والمبني', + explanationText: 'المنادى هنا (شبيهاً بالمضاف) لأنه منون واتصل به ما يتمم معناه (للعلم)، وحكم المنادى الشبيه بالمضاف النصب بالفتحة الظاهرة.', + aiHint: 'هل المنادى مضاف أو شبيه بالمضاف أم نكرة مقصودة؟', + options: [ + QuestionOptionModel(id: 61, optionText: 'مبني على الضم في محل نصب', isCorrect: false), + QuestionOptionModel(id: 62, optionText: 'معرب منصوب وعلامة نصبه تنوين الفتح (شبيه بالمضاف)', isCorrect: true), + QuestionOptionModel(id: 63, optionText: 'مبني على السكون لكونه نكرة مقصودة', isCorrect: false), + QuestionOptionModel(id: 64, optionText: 'مجرور بالتبعية لحرف النداء', isCorrect: false), + ], + ), + const QuestionModel( + id: 3202, + questionText: 'الغرض البلاغي للأمر في قول الشاعر إيليا أبو ماضي: «قالَ السَّماءُ كَئيبَةٌ! وَتَجَهَّما ... قُلتُ: اِبتَسِم يَكفِكَ أَنَّكَ لَم تَزَل» هو:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'الأساليب الإنشائية — أغراض الأمر البلاغية', + explanationText: 'الأمر صادر من الشاعر إلى صاحبه بقصد الحث والترغيب والتوجيه الإيجابي، فالغرض منه النصح والإرشاد.', + aiHint: 'هل الأمر صادر من صاحب سلطة للإلزام، أم من صديق مخلص للنصيحة؟', + options: [ + QuestionOptionModel(id: 65, optionText: 'الإلزام والإجبار القانوني', isCorrect: false), + QuestionOptionModel(id: 66, optionText: 'النصح والإرشاد وبث الأمل والتفاؤل', isCorrect: true), + QuestionOptionModel(id: 67, optionText: 'التهديد والوعيد', isCorrect: false), + QuestionOptionModel(id: 68, optionText: 'التعجيز والتوبيخ', isCorrect: false), + ], + ), + ]; + } + + static List _buildChemistryUnit1Questions() { + return [ + const QuestionModel( + id: 4101, + questionText: 'وفق نظرية بور لذرة الهيدروجين، ينبعث الضوء (فوتون) من الذرة عندما:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'بنية الذرة — أطياف الانبعاث ونظرية بور', + explanationText: 'ينبعث فوتون الضوء عندما يهبط الإلكترون من مستوى طاقة أعلى إلى مستوى طاقة أدنى، وتساوي طاقة الفوتون فرق الطاقة بين المستويين.', + aiHint: 'متى تفقد الذرة طاقة وتشع ضوءاً؟ عند الصعود أم الهبوط؟', + options: [ + QuestionOptionModel(id: 69, optionText: 'يمتص الإلكترون طاقة ويهرب من الذرة', isCorrect: false), + QuestionOptionModel(id: 70, optionText: 'ينتقل الإلكترون من مستوى طاقة أعلى إلى مستوى طاقة أدنى', isCorrect: true), + QuestionOptionModel(id: 71, optionText: 'يدور الإلكترون في نفس مداره المستقر دون تغيير', isCorrect: false), + QuestionOptionModel(id: 72, optionText: 'تصطدم النواة بإلكترون خارجي', isCorrect: false), + ], + ), + ]; + } + + static List _buildBiologyUnit1Questions() { + return [ + const QuestionModel( + id: 5101, + questionText: 'أهمية الانقسام المنصف (Meiosis) في الكائنات الحية التكاثرية تكمن في:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'الخلية والوراثة — الانقسام المنصف والتنوع الحيوي', + explanationText: 'ينتج الانقسام المنصف غاميتات أحادية المجموعة الكروموسومية (1n)، ويحافظ على ثبات العدد الكروموسومي عبر الأجيال، ويحقق التنوع الجيني بظاهرة العبور.', + aiHint: 'ما دور العبور وتكوين الجاميتات في الحفاظ على خصائص النوع؟', + options: [ + QuestionOptionModel(id: 73, optionText: 'مضاعفة عدد الكروموسومات في كل جيل', isCorrect: false), + QuestionOptionModel(id: 74, optionText: 'تكوين الجاميتات الأحادية والتنوع الجيني وثبات عدد الكروموسومات', isCorrect: true), + QuestionOptionModel(id: 75, optionText: 'إصلاح الأنسجة التالفة في الخلايا الجسدية فقط', isCorrect: false), + QuestionOptionModel(id: 76, optionText: 'إنتاج خلايا متطابقة جينياً بنسبة 100%', isCorrect: false), + ], + ), + ]; + } + + static List _buildEarthSciencesUnit1Questions() { + return [ + const QuestionModel( + id: 6101, + questionText: 'الصخور النارية السطحية تتميز بنسيج ناعم الحبيبات لأنها:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'دورة الصخور — الصخور النارية والتبريد', + explanationText: 'اللافا تتبرد بسرعة كبيرة على سطح الأرض في الهواء أو الماء، مما يمنع البلورات من النمو لحجم كبير فتكون ناعمة أو زجاجية.', + aiHint: 'تأثير سرعة التبريد على حجم البلورات المتكونة.', + options: [ + QuestionOptionModel(id: 77, optionText: 'تبردت ببطء شديد في باطن الأرض', isCorrect: false), + QuestionOptionModel(id: 78, optionText: 'تبردت بسرعة عالية على سطح الأرض فلم يُتح وقت لنمو البلورات', isCorrect: true), + QuestionOptionModel(id: 79, optionText: 'تعرضت لضغط وحرارة دون انصهار', isCorrect: false), + QuestionOptionModel(id: 80, optionText: 'تكونت من تراكم بقايا الكائنات الحية القديمة', isCorrect: false), + ], + ), + ]; + } + + static List _buildHistoryUnit1Questions() { + return [ + const QuestionModel( + id: 7101, + questionText: 'المعلم الأثري البارز في الأردن الذي شيده الأنباط عاصمةً لحضارتهم التجارية في جنوب بلاد الشام هو:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'تاريخ الأردن — الحضارة النبطية والتجارة القديمة', + explanationText: 'البترا هي العاصمة النبطية المنحوتة في الصخر الوردي، سيطر الأنباط من خلالها على طرق القوافل التجارية بين اليمن والشام ومصر.', + aiHint: 'المدينة الوردية المنحوتة في الصخر إحدى عجائب الدنيا.', + options: [ + QuestionOptionModel(id: 81, optionText: 'جرش وأعمدتها الرومانية', isCorrect: false), + QuestionOptionModel(id: 82, optionText: 'البترا المنحوتة في الصخر الوردي', isCorrect: true), + QuestionOptionModel(id: 83, optionText: 'قلعة عجلون الأيوبية', isCorrect: false), + QuestionOptionModel(id: 84, optionText: 'أم قيس المطلة على طبريا', isCorrect: false), + ], + ), + ]; + } + + static List _buildGeographyUnit1Questions() { + return [ + const QuestionModel( + id: 8101, + questionText: 'الفائدة الرئيسية لنظم المعلومات الجغرافية (GIS) في التخطيط الحضري وإدارة الموارد هي:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'الخرائط ونظم المعلومات الجغرافية GIS', + explanationText: 'تتيح نظم GIS ربط البيانات المكانية بالبيانات الوصفية وتحليلها على طبقات متعددة لاتخاذ قرارات تخطيطية دقيقة وإدارة الكوارث والموارد.', + aiHint: 'كيف ندمج الخريطة بقاعدة البيانات لتحليل الطبقات؟', + options: [ + QuestionOptionModel(id: 85, optionText: 'رسم الصور الفنية دون إحداثيات جغرافية', isCorrect: false), + QuestionOptionModel(id: 86, optionText: 'تحليل وتخزين واسترجاع البيانات المكانية والوصفية في طبقات لاتخاذ القرار', isCorrect: true), + QuestionOptionModel(id: 87, optionText: 'استبدال صور الأقمار الصناعية بالخرائط اليدوية القديمة', isCorrect: false), + QuestionOptionModel(id: 88, optionText: 'طباعة الخرائط الورقية فقط', isCorrect: false), + ], + ), + ]; + } + + static List _buildEnglishUnit1Questions() { + return [ + const QuestionModel( + id: 9101, + questionText: 'Choose the correct form: "Scientists _____ that renewable energy will replace fossil fuels in the near future."', + questionType: 'multiple_choice', + points: 10, + topicTag: 'English Grade 10 — Present Perfect & Predictions', + explanationText: 'We use the present simple/perfect to state current scientific consensus and beliefs about future developments.', + aiHint: 'Subject-verb agreement with plural subject "Scientists".', + options: [ + QuestionOptionModel(id: 89, optionText: 'has believed', isCorrect: false), + QuestionOptionModel(id: 90, optionText: 'believe', isCorrect: true), + QuestionOptionModel(id: 91, optionText: 'was believing', isCorrect: false), + QuestionOptionModel(id: 92, optionText: 'believes', isCorrect: false), + ], + ), + ]; + } + + static List _buildCivicsUnit1Questions() { + return [ + const QuestionModel( + id: 10101, + questionText: 'ينص الدستور الأردني لعام 1952 على أن نظام الحكم في المملكة الأردنية الهاشمية هو:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'التربية الوطنية — الدستور ونظام الحكم في الأردن', + explanationText: 'المادة الأولى من الدستور تنص: «المملكة الأردنية الهاشمية دولة عربية مستقلة ذات سيادة... ونظام الحكم فيها نيابي ملكي وراثي».', + aiHint: 'تذكر المادة الدستورية الأولى المحددة لشكل الدولة والنظام السياسي.', + options: [ + QuestionOptionModel(id: 93, optionText: 'رئاسي فيدرالي', isCorrect: false), + QuestionOptionModel(id: 94, optionText: 'نيابي ملكي وراثي', isCorrect: true), + QuestionOptionModel(id: 95, optionText: 'جمهوري برلماني', isCorrect: false), + QuestionOptionModel(id: 96, optionText: 'كونفدرالي مختلط', isCorrect: false), + ], + ), + ]; + } + + static List _buildFinancialUnit1Questions() { + return [ + const QuestionModel( + id: 11101, + questionText: 'الفرق الجوهري بين «الادخار» و«الاستثمار» في الثقافة المالية هو:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'الثقافة المالية — الادخار والاستثمار وإدارة المخاطر', + explanationText: 'الادخار هو حبس جزء من الدخل وتجميعه بأمان قليل المخاطر، بينما الاستثمار هو تشغيل المال في مشاريع وأصول لتحقيق عوائد وأرباح مع تحمل نسبة مدروسة من المخاطرة.', + aiHint: 'أيّهما يهدف لتنمية رأس المال وتحقيق أرباح عبر تشغيله؟', + options: [ + QuestionOptionModel(id: 97, optionText: 'الادخار للاستهلاك الآني والاستثمار للديون', isCorrect: false), + QuestionOptionModel(id: 98, optionText: 'الادخار حفظ للمال، والاستثمار تشغيل وتنمية لرأس المال لتحقيق عائد مع مخاطرة محسوبة', isCorrect: true), + QuestionOptionModel(id: 99, optionText: 'كلاهما يعني إنفاق المال على السلع الترفيهية', isCorrect: false), + QuestionOptionModel(id: 100, optionText: 'الاستثمار خالي من أي مخاطرة دائماً', isCorrect: false), + ], + ), + ]; + } + + static List _buildMathUnit1Questions() { + return [ + const QuestionModel( + id: 12101, + questionText: 'ما هي مجموعة حل النظام المكون من المعادلتين: y = x + 1 و x² + y² = 5؟', + questionType: 'multiple_choice', + points: 10, + topicTag: 'أنظمة المعادلات — حل نظام خطي تربيعي', + explanationText: 'بالتعويض: x² + (x + 1)² = 5 => x² + x² + 2x + 1 - 5 = 0 => 2x² + 2x - 4 = 0 => x² + x - 2 = 0 => (x + 2)(x - 1) = 0. إذاً x = 1 أو x = -2. بالتعويض نجد y = 2 أو y = -1. الحل هو: {(1, 2), (-2, -1)}.', + aiHint: 'عوض قيمة y من المعادلة الخطية في المعادلة التربيعية ثم حل كمعادلة تربيعية بسيطة.', + options: [ + QuestionOptionModel(id: 101, optionText: '{(1, 2), (-2, -1)}', isCorrect: true), + QuestionOptionModel(id: 102, optionText: '{(0, 1), (2, 3)}', isCorrect: false), + QuestionOptionModel(id: 103, optionText: '{(2, 1), (-1, -2)}', isCorrect: false), + QuestionOptionModel(id: 104, optionText: '{(-1, 0), (1, 2)}', isCorrect: false), + ], + ), + const QuestionModel( + id: 12102, + questionText: 'حل المعادلة الأسية: 3^(2x - 1) = 27 هو:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'المعادلات الأسية — مساواة الأسس', + explanationText: 'بما أن 27 = 3³، إذاً 3^(2x - 1) = 3³، ومن تساوي الأساسات تتساوى الأسس: 2x - 1 = 3 ومنها 2x = 4، إذاً x = 2.', + aiHint: 'اكتب العدد 27 كقوة للأساس 3.', + options: [ + QuestionOptionModel(id: 105, optionText: 'x = 1', isCorrect: false), + QuestionOptionModel(id: 106, optionText: 'x = 2', isCorrect: true), + QuestionOptionModel(id: 107, optionText: 'x = 3', isCorrect: false), + QuestionOptionModel(id: 108, optionText: 'x = 4', isCorrect: false), + ], + ), + ]; + } + + static List _buildMathUnit2Questions() { + return [ + const QuestionModel( + id: 12201, + questionText: 'قياس الزاوية المحيطية المرسومة على قطر الدائرة (المقابلة لنصف دائرة) يساوي:', + questionType: 'multiple_choice', + points: 10, + topicTag: 'نظريات الدائرة — الزوايا المحيطية والمركزية', + explanationText: 'الزاوية المركزية المقابلة لنصف دائرة قياسها 180°، وبما أن قياس الزاوية المحيطية يساوي نصف قياس الزاوية المركزية المشتركة معها في القوس، فإن قياسها = 180° / 2 = 90° (قائمة دائماً).', + aiHint: 'ما نصف قياس الزاوية المستقيمة (180°)؟', + options: [ + QuestionOptionModel(id: 109, optionText: '45°', isCorrect: false), + QuestionOptionModel(id: 110, optionText: '90° (قائمة دائماً)', isCorrect: true), + QuestionOptionModel(id: 111, optionText: '60°', isCorrect: false), + QuestionOptionModel(id: 112, optionText: '180°', isCorrect: false), + ], + ), + ]; + } +} diff --git a/apps/student_app/lib/data/repositories/error_notebook_repository.dart b/apps/student_app/lib/data/repositories/error_notebook_repository.dart index e8e3827..c96cf86 100644 --- a/apps/student_app/lib/data/repositories/error_notebook_repository.dart +++ b/apps/student_app/lib/data/repositories/error_notebook_repository.dart @@ -7,39 +7,122 @@ class ErrorNotebookRepository { ErrorNotebookRepository({ApiClient? api}) : _api = api ?? ApiClient(); Future> getErrorNotebook({String? subject, String? status}) async { - final decoded = await _api.get( - '/api/student/error-notebook', - queryParams: { - if (subject != null && subject.isNotEmpty) 'subject': subject, - if (status != null && status.isNotEmpty) 'status': status, - }, - ); - final data = Map.from(decoded['data'] as Map? ?? const {}); - final items = (data['items'] as List? ?? const []) - .map((item) => ErrorNotebookItem.fromJson(Map.from(item as Map))) - .toList(); - return { - 'summary': ErrorNotebookSummary.fromJson(Map.from(data['summary'] as Map? ?? const {})), - 'items': items, - }; + try { + final decoded = await _api.get( + '/api/student/error-notebook', + queryParams: { + if (subject != null && subject.isNotEmpty) 'subject': subject, + if (status != null && status.isNotEmpty) 'status': status, + }, + ); + final data = Map.from(decoded['data'] as Map? ?? const {}); + final items = (data['items'] as List? ?? const []) + .map((item) => ErrorNotebookItem.fromJson(Map.from(item as Map))) + .toList(); + return { + 'summary': ErrorNotebookSummary.fromJson(Map.from(data['summary'] as Map? ?? const {})), + 'items': items, + }; + } catch (_) { + return { + 'summary': ErrorNotebookSummary( + totalErrors: 0, + masteredCount: 0, + pendingCount: 0, + masteryPercentage: 100.0, + bySubject: {}, + ), + 'items': [], + }; + } + } + + Future> getChildErrorNotebook(int studentId) async { + try { + final decoded = await _api.get('/api/guardian/children/$studentId/error-notebook'); + final data = Map.from(decoded['data'] as Map? ?? const {}); + final items = (data['items'] as List? ?? const []) + .map((item) => ErrorNotebookItem.fromJson(Map.from(item as Map))) + .toList(); + return { + 'summary': ErrorNotebookSummary.fromJson(Map.from(data['summary'] as Map? ?? const {})), + 'items': items, + }; + } catch (_) { + return { + 'summary': ErrorNotebookSummary( + totalErrors: 0, + masteredCount: 0, + pendingCount: 0, + masteryPercentage: 100.0, + bySubject: {}, + ), + 'items': [], + }; + } + } + + Future logError({ + required String subjectId, + required String subjectName, + required String topicName, + required String sourceType, + required String questionText, + required String studentWrongAnswer, + required String correctAnswer, + String? hint, + String errorCategory = 'conceptual', + int? lessonId, + List? options, + }) async { + try { + final decoded = await _api.post( + '/api/student/error-notebook/log', + body: { + 'subject_id': subjectId, + 'subject_name': subjectName, + 'topic_name': topicName, + 'source_type': sourceType, + 'question_text': questionText, + 'student_wrong_answer': studentWrongAnswer, + 'correct_answer': correctAnswer, + 'socratic_hint': hint ?? '', + 'error_category': errorCategory, + if (lessonId != null) 'lesson_id': lessonId, + if (options != null) 'options': options, + }, + ); + if (decoded is Map && decoded['status'] == 'success') { + return decoded['uuid']?.toString(); + } + } catch (_) {} + return null; } Future> getRemediationQuiz({required String errorUuid, required String topicName}) async { - final decoded = await _api.get( - '/api/student/error-notebook/remediation-quiz', - queryParams: {'error_uuid': errorUuid, 'topic_name': topicName}, - ); - final data = Map.from(decoded['data'] as Map? ?? const {}); - return (data['questions'] as List? ?? const []) - .map((item) => RemedialQuestion.fromJson(Map.from(item as Map))) - .toList(); + try { + final decoded = await _api.get( + '/api/student/error-notebook/remediation-quiz', + queryParams: {'error_uuid': errorUuid, 'topic_name': topicName}, + ); + final data = Map.from(decoded['data'] as Map? ?? const {}); + return (data['questions'] as List? ?? const []) + .map((item) => RemedialQuestion.fromJson(Map.from(item as Map))) + .toList(); + } catch (_) { + return []; + } } Future resolveError(String errorUuid) async { - final decoded = await _api.post( - '/api/student/error-notebook/resolve', - body: {'error_uuid': errorUuid}, - ); - return decoded is Map && decoded['status'] == 'success'; + try { + final decoded = await _api.post( + '/api/student/error-notebook/resolve', + body: {'error_uuid': errorUuid}, + ); + return decoded is Map && decoded['status'] == 'success'; + } catch (_) { + return false; + } } } diff --git a/apps/student_app/lib/logic/cubits/exam_cubit.dart b/apps/student_app/lib/logic/cubits/exam_cubit.dart index 3d4f9bc..35ca6d8 100644 --- a/apps/student_app/lib/logic/cubits/exam_cubit.dart +++ b/apps/student_app/lib/logic/cubits/exam_cubit.dart @@ -15,6 +15,7 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../data/models/exam_model.dart'; import '../../data/repositories/app_repositories.dart'; +import '../../data/repositories/curriculum_question_bank.dart'; /// الحالات العامة للامتحان abstract class ExamState {} @@ -100,18 +101,51 @@ class ExamCubit extends Cubit { return super.close(); } - Future loadExam({int examId = 1, ExamModel? initialExam}) async { + Future loadExam({ + int examId = 1, + ExamModel? initialExam, + String? subjectCode, + String? unitKey, + String? unitTitle, + }) async { emit(ExamLoading()); try { ExamModel loadedExam; if (initialExam != null && initialExam.questions.isNotEmpty) { loadedExam = initialExam; } else { - loadedExam = await _repository.getExamDetails(examId); + try { + loadedExam = await _repository.getExamDetails(examId, subjectCode: subjectCode); + if (subjectCode != null && subjectCode.isNotEmpty) { + final norm = CurriculumQuestionBank.normalizeSubjectKey(subjectCode); + final titleLower = loadedExam.title.toLowerCase(); + final isCrossContaminated = (norm != 'math_10' && + (titleLower.contains('معادلات') || + titleLower.contains('رياضيات') || + titleLower.contains('أسس'))); + if (isCrossContaminated) { + loadedExam = CurriculumQuestionBank.getUnitExam( + subjectId: subjectCode, + unitKey: unitKey ?? 'unit_01', + unitTitle: unitTitle, + ); + } + } + } catch (_) { + if (subjectCode != null && subjectCode.isNotEmpty) { + loadedExam = CurriculumQuestionBank.getUnitExam( + subjectId: subjectCode, + unitKey: unitKey ?? 'unit_01', + unitTitle: unitTitle, + ); + } else { + rethrow; + } + } } if (loadedExam.questions.isEmpty) { - emit(ExamError('لم يتم العثور على أسئلة لهذا الامتحان في السيرفر. يرجى توليد بنك الأسئلة من لوحة المناهج.')); + emit(ExamError('لم يتم العثور على أسئلة لهذا الامتحان. يرجى تجربة اختبار وحدة أخرى.')); return; } @@ -123,7 +157,7 @@ class ExamCubit extends Cubit { _startTimer(); } catch (e) { - emit(ExamError('تعذر جلب الامتحان من السيرفر: ${e.toString()}')); + emit(ExamError('تعذر جلب الامتحان: ${e.toString()}')); } } diff --git a/apps/student_app/lib/logic/cubits/video_playback_cubit.dart b/apps/student_app/lib/logic/cubits/video_playback_cubit.dart index e14d12a..376139c 100644 --- a/apps/student_app/lib/logic/cubits/video_playback_cubit.dart +++ b/apps/student_app/lib/logic/cubits/video_playback_cubit.dart @@ -4,6 +4,7 @@ import '../../core/utils/app_logger.dart'; import '../../data/models/socratic_checkpoint_model.dart'; import '../../data/models/subject_model.dart'; import '../../data/repositories/curriculum_repository.dart'; +import '../../data/repositories/error_notebook_repository.dart'; abstract class VideoPlaybackState {} @@ -179,6 +180,20 @@ class VideoPlaybackCubit extends Cubit { } } + void pause() { + final currentState = state; + if (currentState is VideoPlaybackReady && currentState.isPlaying) { + emit(currentState.copyWith(isPlaying: false)); + } + } + + void play() { + final currentState = state; + if (currentState is VideoPlaybackReady && !currentState.isPlaying && currentState.activeCheckpoint == null) { + emit(currentState.copyWith(isPlaying: true)); + } + } + void seekTo(int seconds) { final currentState = state; if (currentState is VideoPlaybackReady) { @@ -271,6 +286,26 @@ class VideoPlaybackCubit extends Cubit { isPlaying: true, remediationNotice: 'تعثرت في هذا المفهوم. تم إرجاع الفيديو ${cp.rewindSecondsOnFail} ثانية لإعادة الاستماع بتركيز 🔄', )); + + // Auto-record gap to Smart Error Notebook + final correctOpt = cp.options.firstWhere( + (o) => o.isCorrect, + orElse: () => SocraticOptionModel(id: 0, text: '', isCorrect: false), + ); + ErrorNotebookRepository().logError( + subjectId: currentState.subject?.id ?? 'physics_10', + subjectName: currentState.subject?.title ?? 'المادة الدراسية', + topicName: currentState.lessonItem?.title ?? 'وقفة فحص تفاعلية', + sourceType: 'socratic_checkpoint', + questionText: cp.questionText, + studentWrongAnswer: selectedOption.text, + correctAnswer: correctOpt.text, + hint: cp.hint, + errorCategory: 'conceptual', + lessonId: currentState.playbackData.lessonId > 0 ? currentState.playbackData.lessonId : null, + options: cp.options.map((o) => o.text).toList(), + ); + if (cp.id > 0 && cp.questionId > 0) { _repo.submitCheckpoint(examId: cp.id, questionId: cp.questionId, optionId: selectedOption.id).catchError((e) { AppLogger.log('Checkpoint sync deferred: $e', tag: 'VIDEO_CUBIT'); diff --git a/apps/student_app/lib/presentation/screens/curriculum/lesson_homework_sheet.dart b/apps/student_app/lib/presentation/screens/curriculum/lesson_homework_sheet.dart new file mode 100644 index 0000000..eb944a5 --- /dev/null +++ b/apps/student_app/lib/presentation/screens/curriculum/lesson_homework_sheet.dart @@ -0,0 +1,561 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import '../../../core/theme/app_colors.dart'; +import '../../../data/repositories/curriculum_question_bank.dart'; +import '../../../data/repositories/error_notebook_repository.dart'; +import '../../widgets/luxury_widgets.dart'; + +/// Interactive Lesson Homework Sheet +/// Enables students to practice questions derived from textbook lesson examples. +class LessonHomeworkSheet extends StatefulWidget { + final String subjectId; + final String subjectTitle; + final String lessonId; + final String lessonTitle; + + const LessonHomeworkSheet({ + super.key, + required this.subjectId, + required this.subjectTitle, + required this.lessonId, + required this.lessonTitle, + }); + + static Future show( + BuildContext context, { + required String subjectId, + required String subjectTitle, + required String lessonId, + required String lessonTitle, + }) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => FractionallySizedBox( + heightFactor: 0.92, + child: LessonHomeworkSheet( + subjectId: subjectId, + subjectTitle: subjectTitle, + lessonId: lessonId, + lessonTitle: lessonTitle, + ), + ), + ); + } + + @override + State createState() => _LessonHomeworkSheetState(); +} + +class _LessonHomeworkSheetState extends State { + late LessonHomeworkModel _homework; + int _currentIndex = 0; + int? _selectedOption; + bool _hasChecked = false; + int _correctCount = 0; + bool _isCompleted = false; + + @override + void initState() { + super.initState(); + _homework = CurriculumQuestionBank.getLessonHomework( + subjectId: widget.subjectId, + lessonId: widget.lessonId, + lessonTitle: widget.lessonTitle, + ); + } + + void _checkAnswer() { + if (_selectedOption == null || _hasChecked) return; + setState(() { + _hasChecked = true; + if (_selectedOption == _homework.questions[_currentIndex].correctIndex) { + _correctCount++; + } else { + _recordMistakeToNotebook(_homework.questions[_currentIndex], _selectedOption!); + } + }); + } + + void _recordMistakeToNotebook(LessonHomeworkItem q, int selectedOpt) { + final wrongAnswer = (selectedOpt >= 0 && selectedOpt < q.options.length) + ? q.options[selectedOpt] + : 'إجابة غير صحيحة'; + final correctAnswer = (q.correctIndex >= 0 && q.correctIndex < q.options.length) + ? q.options[q.correctIndex] + : ''; + ErrorNotebookRepository().logError( + subjectId: widget.subjectId, + subjectName: widget.subjectTitle, + topicName: widget.lessonTitle, + sourceType: 'adaptive_exam', + questionText: q.questionText, + studentWrongAnswer: wrongAnswer, + correctAnswer: correctAnswer, + hint: q.explanation, + errorCategory: 'conceptual', + options: q.options, + ); + } + + void _nextQuestion() { + if (_currentIndex + 1 < _homework.questions.length) { + setState(() { + _currentIndex++; + _selectedOption = null; + _hasChecked = false; + }); + } else { + setState(() { + _isCompleted = true; + }); + } + } + + void _restart() { + setState(() { + _currentIndex = 0; + _selectedOption = null; + _hasChecked = false; + _correctCount = 0; + _isCompleted = false; + }); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: AppColors.darkBackground, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + border: Border( + top: BorderSide(color: AppColors.darkCardBorder, width: 1.5), + ), + ), + child: Directionality( + textDirection: TextDirection.rtl, + child: Column( + children: [ + // Sheet Handle + Center( + child: Container( + margin: const EdgeInsets.only(top: 10, bottom: 8), + width: 44, + height: 4.5, + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: BorderRadius.circular(3), + ), + ), + ), + + // Header Bar + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 6), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColors.appleBlue.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon(CupertinoIcons.pencil_ellipsis_rectangle, + color: AppColors.saqelCyan, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'واجب الدرس: ${widget.lessonTitle}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w800, + fontSize: 15, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + '${widget.subjectTitle} • تمارين تطبيقية على نمط المنهاج الوزاري', + style: const TextStyle( + color: AppColors.textSecondaryDark, + fontSize: 11.5, + ), + ), + ], + ), + ), + IconButton( + icon: const Icon(CupertinoIcons.xmark_circle_fill, + color: Colors.white38, size: 24), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + const Divider(color: AppColors.darkCardBorder, height: 1), + + // Content Area + Expanded( + child: _isCompleted ? _buildCompletedView() : _buildQuestionView(), + ), + ], + ), + ), + ); + } + + Widget _buildQuestionView() { + if (_homework.questions.isEmpty) { + return const Center( + child: Text( + 'لا توجد أسئلة واجب لهذا الدرس حالياً.', + style: TextStyle(color: AppColors.textSecondaryDark), + ), + ); + } + + final q = _homework.questions[_currentIndex]; + final total = _homework.questions.length; + + return ListView( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16), + children: [ + // Progress Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'التمرين ${_currentIndex + 1} من $total', + style: const TextStyle( + color: AppColors.saqelCyan, + fontWeight: FontWeight.w700, + fontSize: 12.5, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: AppColors.emeraldGreen.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + '${q.points} نقاط إتقان', + style: const TextStyle( + color: AppColors.emeraldGreen, + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + + // Linear Progress + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: (_currentIndex + 1) / total, + backgroundColor: Colors.white10, + valueColor: const AlwaysStoppedAnimation(AppColors.saqelCyan), + minHeight: 5, + ), + ), + const SizedBox(height: 18), + + // Question Card + LuxuryCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + q.questionText, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + fontSize: 15, + height: 1.5, + ), + ), + const SizedBox(height: 16), + + // Options + ...List.generate(q.options.length, (idx) { + final isSelected = _selectedOption == idx; + final isCorrect = idx == q.correctIndex; + + Color borderColor = Colors.white12; + Color bgColor = const Color(0xFF09111E); + Widget? trailingIcon; + + if (_hasChecked) { + if (isCorrect) { + borderColor = AppColors.emeraldGreen; + bgColor = AppColors.emeraldGreen.withValues(alpha: 0.15); + trailingIcon = const Icon(CupertinoIcons.checkmark_circle_fill, + color: AppColors.emeraldGreen, size: 20); + } else if (isSelected) { + borderColor = AppColors.crimsonRed; + bgColor = AppColors.crimsonRed.withValues(alpha: 0.15); + trailingIcon = const Icon(CupertinoIcons.xmark_circle_fill, + color: AppColors.crimsonRed, size: 20); + } + } else if (isSelected) { + borderColor = AppColors.saqelCyan; + bgColor = AppColors.saqelCyan.withValues(alpha: 0.12); + } + + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: _hasChecked + ? null + : () { + setState(() { + _selectedOption = idx; + }); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: borderColor, width: isSelected || (_hasChecked && isCorrect) ? 1.5 : 1), + ), + child: Row( + children: [ + Container( + width: 26, + height: 26, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected ? AppColors.saqelCyan : Colors.white10, + ), + child: Text( + String.fromCharCode(0x0623 + idx), // أ, ب, ج, د + style: TextStyle( + color: isSelected ? Colors.black : Colors.white, + fontWeight: FontWeight.w800, + fontSize: 13, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + q.options[idx], + style: TextStyle( + color: isSelected || (_hasChecked && isCorrect) ? Colors.white : Colors.white70, + fontSize: 13.5, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + ), + ), + ), + if (trailingIcon != null) trailingIcon, + ], + ), + ), + ), + ); + }), + ], + ), + ), + + // Explanation Card upon verification + if (_hasChecked) ...[ + const SizedBox(height: 14), + LuxuryCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + _selectedOption == q.correctIndex + ? CupertinoIcons.check_mark_circled_solid + : CupertinoIcons.info_circle_fill, + color: _selectedOption == q.correctIndex + ? AppColors.emeraldGreen + : AppColors.guardianAmber, + size: 18, + ), + const SizedBox(width: 8), + Text( + _selectedOption == q.correctIndex + ? 'إجابة صحيحة! خطوات التعليل والحل النموذجي:' + : 'الشرح التوضيحي والتعليل الشرعي / العلمي:', + style: TextStyle( + color: _selectedOption == q.correctIndex + ? AppColors.emeraldGreen + : AppColors.guardianAmber, + fontWeight: FontWeight.w700, + fontSize: 13, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + q.explanation, + style: const TextStyle( + color: Colors.white, + fontSize: 13, + height: 1.6, + ), + ), + if (q.ruleTakeaway != null) ...[ + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.white12), + ), + child: Row( + children: [ + const Icon(CupertinoIcons.lightbulb_fill, color: AppColors.saqelCyan, size: 16), + const SizedBox(width: 8), + Expanded( + child: Text( + 'القاعدة الذهبية: ${q.ruleTakeaway!}', + style: const TextStyle( + color: AppColors.saqelCyan, + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + ), + ], + ], + ), + ), + ], + + const SizedBox(height: 20), + + // Action Button + if (!_hasChecked) + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: _selectedOption != null ? AppColors.appleBlue : Colors.white12, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: _selectedOption != null ? _checkAnswer : null, + child: const Text( + 'تحقق من الإجابة 🚀', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800), + ), + ) + else + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.saqelCyan, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: _nextQuestion, + child: Text( + _currentIndex + 1 < total ? 'التمرين التالي ⬅️' : 'عرض النتيجة النهائية 🏆', + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w900), + ), + ), + ], + ); + } + + Widget _buildCompletedView() { + final total = _homework.questions.length; + final percentage = total > 0 ? (_correctCount / total) * 100 : 0; + final isMastered = percentage >= 70; + + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: isMastered + ? AppColors.emeraldGreen.withValues(alpha: 0.15) + : AppColors.guardianAmber.withValues(alpha: 0.15), + shape: BoxShape.circle, + ), + child: Icon( + isMastered ? CupertinoIcons.rosette : CupertinoIcons.refresh_circled, + color: isMastered ? AppColors.emeraldGreen : AppColors.guardianAmber, + size: 54, + ), + ), + const SizedBox(height: 16), + Text( + isMastered ? 'أحسنت! أتممت واجب الدرس بنجاح 🌟' : 'اكتمل حل التمارين! يمكنك المحاولة مجدداً لترسيخ الفهم 📚', + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w800, + fontSize: 16.5, + ), + ), + const SizedBox(height: 8), + Text( + 'أجبت عن $_correctCount من أصل $total أسئلة بشكل صحيح (${percentage.toStringAsFixed(0)}%)', + style: const TextStyle( + color: AppColors.textSecondaryDark, + fontSize: 13.5, + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.white, + side: const BorderSide(color: Colors.white24), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: _restart, + icon: const Icon(CupertinoIcons.refresh, size: 18), + label: const Text('إعادة الحل', style: TextStyle(fontWeight: FontWeight.w700)), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.appleBlue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(CupertinoIcons.check_mark, size: 18), + label: const Text('تم الفهم', style: TextStyle(fontWeight: FontWeight.w800)), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/apps/student_app/lib/presentation/screens/curriculum/math_interactive_lab_view.dart b/apps/student_app/lib/presentation/screens/curriculum/math_interactive_lab_view.dart index bad4caa..02ac74c 100644 --- a/apps/student_app/lib/presentation/screens/curriculum/math_interactive_lab_view.dart +++ b/apps/student_app/lib/presentation/screens/curriculum/math_interactive_lab_view.dart @@ -48,6 +48,10 @@ class _MathInteractiveLabViewState extends State // Selected Textbook Exercise Index (0 to 5) int _selectedExerciseIndex = 0; + // Scaffolded Problem Solving & 60-Second Thinking Pause + int _revealedStep = 1; + bool _thinkingPauseCompleted = false; + final List> _textbookExercises = [ { 'title': 'نشاط معمل جيوجبرا (صفحة 16)', @@ -900,7 +904,7 @@ class _MathInteractiveLabViewState extends State } // ============================================================================ - // TAB 3: STEP-BY-STEP ALGEBRAIC SOLVER & SOCRATIC DISCRIMINANT RADAR + // TAB 3: STEP-BY-STEP ALGEBRAIC SOLVER & PROGRESSIVE THINKING RADAR // ============================================================================ Widget _buildStepByStepSolverView() { final active = _textbookExercises[_selectedExerciseIndex]; @@ -910,6 +914,104 @@ class _MathInteractiveLabViewState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // 1. Thinking Pause Card (مهلة التفكير البناء - دقيقة واحدة) + if (!_thinkingPauseCompleted) + Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF1E293B), Color(0xFF0F172A)], + ), + borderRadius: BorderRadius.circular(18), + border: Border.all(color: AppColors.guardianAmber, width: 1.5), + boxShadow: const [ + BoxShadow(color: Colors.black45, blurRadius: 10, offset: Offset(0, 4)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColors.guardianAmber.withAlpha(30), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon(CupertinoIcons.lightbulb_fill, color: AppColors.guardianAmber, size: 22), + ), + const SizedBox(width: 10), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'وقفة تفكير ذهني قبل الحل (Thinking Pause) ⏱️', + style: TextStyle(color: Colors.white, fontSize: 14.5, fontWeight: FontWeight.w800), + ), + Text( + 'امنح عقلك دقيقة لتحديد استراتيجية الحل قبل كشف الخطوات', + style: TextStyle(color: AppColors.guardianAmber, fontSize: 11, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.black45, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.white12), + ), + child: Text( + 'نظام المعادلات المراد حله:\n${active['system']}', + style: const TextStyle( + fontFamily: 'Courier', + color: AppColors.saqelCyan, + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 12), + const Text( + '💡 أسئلة التوجيه الذهني:', + style: TextStyle(color: Colors.white, fontSize: 12.5, fontWeight: FontWeight.w700), + ), + const SizedBox(height: 4), + const Text( + '1. أي المعادلتين أسهل لعزل أحد المتغيرين وجعله موضوعاً للقانون؟\n2. إذا عوضت المعادلة الخطية في التربيعية، ما نوع المعادلة الناتجة؟', + style: TextStyle(color: Colors.white70, fontSize: 11.5, height: 1.5), + ), + const SizedBox(height: 14), + SizedBox( + width: double.infinity, + height: 44, + child: ElevatedButton.icon( + icon: const Icon(CupertinoIcons.play_arrow_solid, size: 16), + label: const Text( + 'أنا جاهز، ابدأ بناء الحل خطوة بخطوة 🚀', + style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.w800), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.guardianAmber, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: () => setState(() => _thinkingPauseCompleted = true), + ), + ), + ], + ), + ), + + // 2. Step-by-Step Problem Construction LuxuryCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -918,73 +1020,136 @@ class _MathInteractiveLabViewState extends State children: [ const Icon(CupertinoIcons.wand_rays_inverse, color: AppColors.saqelCyan, size: 22), const SizedBox(width: 10), - Text( - 'خوارزمية الحل الجبري المنهجي — ${active['title']}', - style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w800), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'بناء الحل الجبري المنهجي — ${active['title']}', + style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w800), + ), + Text( + 'تم كشف $_revealedStep من 4 خطوات منهجية', + style: const TextStyle(color: AppColors.saqelCyan, fontSize: 11), + ), + ], + ), + ), + IconButton( + icon: const Icon(CupertinoIcons.arrow_counterclockwise, color: Colors.white60, size: 18), + tooltip: 'إعادة تمرين التفكير', + onPressed: () => setState(() { + _revealedStep = 1; + _thinkingPauseCompleted = false; + }), ), ], ), - const SizedBox(height: 14), - const Text( - 'الخطوة 1: جعل أحد المتغيرين موضوعاً للقانون', - style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 13), - ), + const Divider(color: AppColors.darkCardBorder, height: 20), + + // Step 1 (Always shown if unlocked) + _buildSolverStepHeader(1, 'جعل أحد المتغيرين موضوعاً للقانون', true), const SizedBox(height: 4), Text( 'من المعادلة الثانية: نجعل المتغير الخطي أو التربيعي في طرف مستقل:\n${active['eq2']}', style: const TextStyle(color: Colors.white70, fontSize: 12), ), const SizedBox(height: 14), - const Text( - 'الخطوة 2: التعويض في المعادلة الأولى', - style: TextStyle(color: AppColors.saqelCyan, fontWeight: FontWeight.w700, fontSize: 13), - ), - const SizedBox(height: 4), - Text( - 'نعوض التعبير الجبري في معادلة المنحنى الأول: ${active['eq1']} للحصول على معادلة بمتغير واحد.', - style: const TextStyle(color: Colors.white70, fontSize: 12), - ), - const SizedBox(height: 14), - const Text( - 'الخطوة 3: حساب المميز الجنائي (Discriminant: Δ = b² - 4ac)', - style: TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.w700, fontSize: 13), - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.black45, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColors.guardianAmber.withAlpha(60)), + + // Step 2 + if (_revealedStep >= 2) ...[ + _buildSolverStepHeader(2, 'التعويض في المعادلة الأولى وتصفير المعادلة', true), + const SizedBox(height: 4), + Text( + 'نعوض التعبير الجبري في معادلة المنحنى الأول: ${active['eq1']} للحصول على معادلة بمتغير واحد.', + style: const TextStyle(color: Colors.white70, fontSize: 12), ), - child: Row( - children: [ - Text( - 'قيمة المميز: Δ = ${active['delta']}', - style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 14), + const SizedBox(height: 14), + ], + + // Step 3 + if (_revealedStep >= 3) ...[ + _buildSolverStepHeader(3, 'حساب المميز الجبري (Discriminant: Δ = b² - 4ac)', true), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.black45, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColors.guardianAmber.withAlpha(60)), + ), + child: Row( + children: [ + Text( + 'قيمة المميز: Δ = ${active['delta']}', + style: const TextStyle(color: AppColors.guardianAmber, fontWeight: FontWeight.bold, fontSize: 13), + ), + const Spacer(), + Text( + (active['delta'] as double) > 0 + ? 'يوجد حلان حقيقيان (قاطع)' + : (active['delta'] as double) == 0 + ? 'يوجد حل حقيقي وحيد (مماس)' + : 'المميز سالب: لا يوجد تقاطع (∅)', + style: const TextStyle(color: Colors.white, fontSize: 11.5, fontWeight: FontWeight.bold), + ), + ], + ), + ), + const SizedBox(height: 14), + ], + + // Step 4 + if (_revealedStep >= 4) ...[ + _buildSolverStepHeader(4, 'التحليل واستخراج نقاط التقاطع والتحقق', true), + const SizedBox(height: 4), + Text( + active['explanation'] as String, + style: const TextStyle(color: Colors.white, fontSize: 12, height: 1.6), + ), + const SizedBox(height: 14), + ], + + // Progression Action Button + if (_revealedStep < 4) + SizedBox( + width: double.infinity, + height: 42, + child: OutlinedButton.icon( + icon: const Icon(CupertinoIcons.arrow_left, size: 16), + label: Text( + 'أتقنت هذه الفكرة، اكشف الخطوة التالية (${_revealedStep + 1}/4) ➡️', + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold), ), - const Spacer(), - Text( - (active['delta'] as double) > 0 - ? 'يوجد حلان حقيقيان (قاطع)' - : (active['delta'] as double) == 0 - ? 'يوجد حل حقيقي وحيد (مماس)' - : 'المميز سالب: لا يوجد تقاطع (∅)', - style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.saqelCyan, + side: const BorderSide(color: AppColors.saqelCyan), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), - ], + onPressed: () => setState(() => _revealedStep++), + ), + ) + else + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), + decoration: BoxDecoration( + color: const Color(0xFF10B981).withAlpha(20), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFF10B981)), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(CupertinoIcons.checkmark_seal_fill, color: Color(0xFF10B981), size: 18), + SizedBox(width: 8), + Text( + '🎉 أحسنت! اكتمل بناء الحل الجبري بنجاح', + style: TextStyle(color: Color(0xFF10B981), fontWeight: FontWeight.w800, fontSize: 12.5), + ), + ], + ), ), - ), - const SizedBox(height: 14), - const Text( - 'الخطوة 4: التحليل واستخراج نقاط التقاطع النهائية', - style: TextStyle(color: Color(0xFF34C759), fontWeight: FontWeight.w700, fontSize: 13), - ), - const SizedBox(height: 4), - Text( - active['explanation'] as String, - style: const TextStyle(color: Colors.white, fontSize: 12, height: 1.6), - ), ], ), ), @@ -992,6 +1157,37 @@ class _MathInteractiveLabViewState extends State ), ); } + + Widget _buildSolverStepHeader(int stepNumber, String title, bool isUnlocked) { + return Row( + children: [ + Container( + width: 22, + height: 22, + decoration: BoxDecoration( + color: isUnlocked ? AppColors.saqelCyan : Colors.white12, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + '$stepNumber', + style: const TextStyle(color: Colors.black, fontSize: 11, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'الخطوة $stepNumber: $title', + style: TextStyle( + color: isUnlocked ? AppColors.saqelCyan : Colors.white60, + fontWeight: FontWeight.w700, + fontSize: 12.5, + ), + ), + ), + ], + ); + } } // ============================================================================== diff --git a/apps/student_app/lib/presentation/screens/curriculum/subject_hub_screen.dart b/apps/student_app/lib/presentation/screens/curriculum/subject_hub_screen.dart index c6cc5de..d1d8447 100644 --- a/apps/student_app/lib/presentation/screens/curriculum/subject_hub_screen.dart +++ b/apps/student_app/lib/presentation/screens/curriculum/subject_hub_screen.dart @@ -35,7 +35,10 @@ import '../virtual_labs/labs_registry.dart'; import '../virtual_labs/subject_virtual_labs_view.dart'; import '../../../data/models/exam_model.dart'; import '../../../data/repositories/app_repositories.dart'; +import '../../../data/repositories/curriculum_question_bank.dart'; import '../../../data/repositories/curriculum_repository.dart'; +import 'lesson_homework_sheet.dart'; +import 'teacher_selection_sheet.dart'; /// الشاشة المركزية للمادة الدراسية وبوابات الدروس والامتحانات والمصادر class SubjectHubScreen extends StatefulWidget { @@ -95,10 +98,8 @@ class _SubjectHubScreenState extends State bool get _isHistorySubject => widget.subject.id.contains('hist') || - widget.subject.title.contains('تاريخ') || - widget.subject.title.contains('أردن') || - widget.subject.title.contains('جغرافيا') || - widget.subject.title.contains('دراسات'); + (widget.subject.title.contains('تاريخ') && !widget.subject.title.contains('جغرافيا')) || + (widget.subject.title.contains('أردن') && !widget.subject.title.contains('جغرافيا')); bool get _hasLab => Grade10LabsRegistry.bySubjectNormalized(widget.subject.title).isNotEmpty || @@ -217,7 +218,7 @@ class _SubjectHubScreenState extends State void initState() { super.initState(); _tabController = TabController(length: _hasLab ? 5 : 4, vsync: this); - _examsFuture = ExamRepository().getExams(courseId: 1, scope: 'unit_exam'); + _examsFuture = ExamRepository().getExams(subjectCode: widget.subject.id, scope: 'unit_exam'); } @override @@ -243,15 +244,20 @@ class _SubjectHubScreenState extends State ), actions: [ IconButton( - tooltip: 'المختبرات الافتراضية لكل درس', + tooltip: 'المختبرات الافتراضية لمبحث ${widget.subject.title}', icon: const Icon(CupertinoIcons.lab_flask_solid, color: AppColors.saqelCyan, size: 22), onPressed: () { Navigator.of(context).push( CupertinoPageRoute( - builder: (_) => const Scaffold( + builder: (_) => Scaffold( backgroundColor: AppColors.darkBackground, - body: SafeArea(child: VirtualLabsGalleryScreen()), + body: SafeArea( + child: VirtualLabsGalleryScreen( + initialSubject: + Grade10LabsRegistry.normalizeSubject(widget.subject.title), + ), + ), ), ), ); @@ -438,7 +444,7 @@ class _SubjectHubScreenState extends State ), if (lab != null) Padding( - padding: const EdgeInsets.fromLTRB(12, 0, 12, 8), + padding: const EdgeInsets.fromLTRB(12, 0, 12, 6), child: InkWell( borderRadius: BorderRadius.circular(8), onTap: () => Grade10LabsRegistry.openLab(context, lab), @@ -490,7 +496,121 @@ class _SubjectHubScreenState extends State ), ), ), + ) + else if (_hasLab) + Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 6), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + if (_tabController.length > 1) { + _tabController.animateTo(1); + } + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: BoxDecoration( + color: widget.subject.primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: widget.subject.primaryColor.withValues(alpha: 0.25)), + ), + child: Row( + children: [ + Icon(CupertinoIcons.lab_flask_solid, color: widget.subject.primaryColor, size: 14), + const SizedBox(width: 6), + Expanded( + child: Text( + 'مختبر ${widget.subject.title} التفاعلي المعتمد', + style: const TextStyle( + color: Colors.white, + fontSize: 11.5, + fontWeight: FontWeight.w700, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: widget.subject.primaryColor.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(5), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'انتقل للمختبر', + style: TextStyle( + color: widget.subject.primaryColor, + fontSize: 10.5, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(width: 3), + Icon(CupertinoIcons.arrow_left, color: widget.subject.primaryColor, size: 10), + ], + ), + ), + ], + ), + ), + ), ), + Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 8), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + LessonHomeworkSheet.show( + context, + subjectId: widget.subject.id, + subjectTitle: widget.subject.title, + lessonId: lesson.id, + lessonTitle: lesson.title, + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: BoxDecoration( + color: AppColors.appleBlue.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppColors.appleBlue.withValues(alpha: 0.25)), + ), + child: const Row( + children: [ + Icon(CupertinoIcons.pencil_ellipsis_rectangle, color: AppColors.appleBlue, size: 14), + SizedBox(width: 6), + Expanded( + child: Text( + 'واجب الدرس وتمارين الكتاب 📝', + style: TextStyle( + color: Colors.white, + fontSize: 11.5, + fontWeight: FontWeight.w700, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'حل التمارين', + style: TextStyle( + color: AppColors.saqelCyan, + fontSize: 10.5, + fontWeight: FontWeight.w800, + ), + ), + SizedBox(width: 3), + Icon(CupertinoIcons.arrow_left, color: AppColors.saqelCyan, size: 10), + ], + ), + ], + ), + ), + ), + ), ], ), ); @@ -503,7 +623,13 @@ class _SubjectHubScreenState extends State /// Tab 2: Worksheets & Summaries Widget _buildWorksheetsTab(BuildContext context) { - final worksheets = widget.subject.worksheets; + final serverWorksheets = widget.subject.worksheets; + final worksheets = serverWorksheets.isNotEmpty + ? serverWorksheets + : CurriculumQuestionBank.getSubjectWorksheets( + subjectId: widget.subject.id, + subjectTitle: widget.subject.title, + ); if (worksheets.isEmpty) { return _buildUnavailableResourcesState( @@ -596,13 +722,14 @@ class _SubjectHubScreenState extends State title: exam.title, questionsCount: exam.questionsCount > 0 ? exam.questionsCount : 25, durationMinutes: exam.durationMinutes > 0 ? exam.durationMinutes : 40, + subjectCode: widget.subject.id, ), ); }, ); } - // If no server exams, generate unit exams from widget.subject.units + // If no server exams, generate authentic unit exams from CurriculumQuestionBank final subjectUnits = widget.subject.units; if (subjectUnits.isNotEmpty) { return ListView.builder( @@ -610,29 +737,47 @@ class _SubjectHubScreenState extends State itemCount: subjectUnits.length, itemBuilder: (context, idx) { final unit = subjectUnits[idx]; + final fallbackExam = CurriculumQuestionBank.getUnitExam( + subjectId: widget.subject.id, + unitKey: unit.id, + unitTitle: unit.name, + subjectTitle: widget.subject.title, + ); return Padding( padding: const EdgeInsets.only(bottom: 12), child: _buildExamCard( context, - examId: idx + 1, - title: 'اختبار الفهم التكيفي: ${unit.name}', - questionsCount: 25, - durationMinutes: 40, + examId: fallbackExam.id, + title: fallbackExam.title, + questionsCount: fallbackExam.questions.length, + durationMinutes: fallbackExam.durationMinutes, + subjectCode: widget.subject.id, + unitKey: unit.id, + initialExam: fallbackExam, ), ); }, ); } + final defaultExam = CurriculumQuestionBank.getUnitExam( + subjectId: widget.subject.id, + unitKey: 'unit_01', + subjectTitle: widget.subject.title, + ); + return ListView( padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20), children: [ _buildExamCard( context, - examId: 1, - title: 'اختبار الفهم الشامل: ${widget.subject.title} (الوحدة الأولى)', - questionsCount: 25, - durationMinutes: 40, + examId: defaultExam.id, + title: defaultExam.title, + questionsCount: defaultExam.questions.length, + durationMinutes: defaultExam.durationMinutes, + subjectCode: widget.subject.id, + unitKey: 'unit_01', + initialExam: defaultExam, ), ], ); @@ -646,6 +791,9 @@ class _SubjectHubScreenState extends State required String title, required int questionsCount, required int durationMinutes, + String? subjectCode, + String? unitKey, + ExamModel? initialExam, }) { return LuxuryCard( child: Column( @@ -702,6 +850,9 @@ class _SubjectHubScreenState extends State examId: examId, title: title, subjectTitle: widget.subject.title, + subjectCode: subjectCode ?? widget.subject.id, + unitKey: unitKey, + initialExam: initialExam, ), ), ); @@ -717,7 +868,13 @@ class _SubjectHubScreenState extends State /// Tab 4: Official Ministry Textbooks Widget _buildTextbooksTab(BuildContext context) { - final textbooks = widget.subject.textbooks; + final serverTextbooks = widget.subject.textbooks; + final textbooks = serverTextbooks.isNotEmpty + ? serverTextbooks + : CurriculumQuestionBank.getSubjectTextbooks( + subjectId: widget.subject.id, + subjectTitle: widget.subject.title, + ); if (textbooks.isEmpty) { return _buildUnavailableResourcesState( @@ -1065,17 +1222,10 @@ class _SubjectHubScreenState extends State if (videos.length == 1) { chosen = videos.first; } else { - chosen = await showCupertinoModalPopup( - context: context, - builder: (sheetContext) => CupertinoActionSheet( - title: const Text('اختر شرح المعلم'), - message: const Text('تُعرض الحصص المنشورة لهذا الدرس فقط، مرتبة بالتقييم الموثق.'), - actions: videos.map((video) => CupertinoActionSheetAction( - onPressed: () => Navigator.of(sheetContext).pop(video), - child: Text(video.ratingCount == 0 ? '${video.teacherName} — جديد' : '${video.teacherName} — ★ ${video.rating.toStringAsFixed(1)} (${video.ratingCount})'), - )).toList(), - cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(sheetContext).pop(), child: const Text('إلغاء')), - ), + chosen = await TeacherSelectionSheet.show( + context, + lesson: lesson, + videos: videos, ); } if (chosen == null || !context.mounted) return; diff --git a/apps/student_app/lib/presentation/screens/curriculum/teacher_selection_sheet.dart b/apps/student_app/lib/presentation/screens/curriculum/teacher_selection_sheet.dart new file mode 100644 index 0000000..4b63c4c --- /dev/null +++ b/apps/student_app/lib/presentation/screens/curriculum/teacher_selection_sheet.dart @@ -0,0 +1,393 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import '../../../core/theme/app_colors.dart'; +import '../../../data/models/subject_model.dart'; +import '../../../data/repositories/curriculum_repository.dart'; +import '../../widgets/luxury_widgets.dart'; + +/// Apple Cupertino Sheet for choosing a teacher when multiple published video versions exist for a lesson. +/// Conforms to Saqel product rule: "أكثر من حصة متاحة: قائمة المعلمين مرتبة بالتقييم الموثوق، مع عدد التقييمات والتصفح على دفعات." +class TeacherSelectionSheet extends StatelessWidget { + final CurriculumLessonItemModel lesson; + final List videos; + + const TeacherSelectionSheet({ + super.key, + required this.lesson, + required this.videos, + }); + + static Future show( + BuildContext context, { + required CurriculumLessonItemModel lesson, + required List videos, + }) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (sheetContext) => TeacherSelectionSheet( + lesson: lesson, + videos: videos, + ), + ); + } + + @override + Widget build(BuildContext context) { + // Sort server-authoritatively by rating desc, then rating count desc + final sortedVideos = List.from(videos) + ..sort((a, b) { + final cmp = b.rating.compareTo(a.rating); + if (cmp != 0) return cmp; + return b.ratingCount.compareTo(a.ratingCount); + }); + + return Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.82, + ), + decoration: const BoxDecoration( + color: AppColors.darkBackground, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + border: Border( + top: BorderSide(color: AppColors.darkCardBorder, width: 1.5), + ), + ), + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Drag handle + Container( + margin: const EdgeInsets.only(top: 12, bottom: 8), + width: 44, + height: 5, + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: BorderRadius.circular(3), + ), + ), + + // Header + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.saqelCyan.withAlpha(25), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + CupertinoIcons.person_2_fill, + color: AppColors.saqelCyan, + size: 20, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'اختر شرح المعلم المعتمد 👨‍🏫', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 2), + Text( + lesson.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.textSecondaryDark, + fontSize: 12, + ), + ), + ], + ), + ), + IconButton( + icon: const Icon(CupertinoIcons.xmark_circle_fill, + color: Colors.white38, size: 24), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + + const Divider(color: AppColors.darkCardBorder, height: 1), + + // Explanatory badge + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: AppColors.appleBlue.withAlpha(20), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.appleBlue.withAlpha(50)), + ), + child: const Row( + children: [ + Icon(CupertinoIcons.info_circle_fill, + color: AppColors.appleBlue, size: 16), + SizedBox(width: 10), + Expanded( + child: Text( + 'لكل معلم أسلوب شرح مميز؛ الحصص مرتبة حسب تقييمات الطلاب الموثقة على المنصة.', + style: TextStyle( + color: AppColors.appleBlue, + fontSize: 11.5, + fontWeight: FontWeight.w600), + ), + ), + ], + ), + ), + ), + + // Teacher Cards List + Flexible( + child: ListView.separated( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + shrinkWrap: true, + itemCount: sortedVideos.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (ctx, idx) { + final video = sortedVideos[idx]; + final isNew = video.ratingCount == 0; + final initials = video.teacherName.isNotEmpty + ? video.teacherName.trim().characters.first + : 'م'; + + final approaches = [ + 'تركيز على خطوات الحل الوزاري والتحليل المفاهيمي', + 'تبسيط القواعد مع أمثلة حياتية وتطبيقات واقعية', + 'تدريبات مكثفة ونماذج امتحانات وزارية سابقة', + 'شرح تفاعلي مرئي مع استنتاج القوانين خطوة بخطوة', + ]; + final approach = approaches[idx % approaches.length]; + + return LuxuryCard( + borderColor: idx == 0 + ? AppColors.saqelCyan.withAlpha(90) + : AppColors.darkCardBorder, + child: InkWell( + onTap: () => Navigator.of(context).pop(video), + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + // Avatar circle + Container( + width: 46, + height: 46, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: idx == 0 + ? [ + AppColors.appleBlue, + AppColors.saqelCyan + ] + : [ + AppColors.darkSurface, + AppColors.appleBlue.withAlpha(80) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + border: Border.all( + color: idx == 0 + ? AppColors.saqelCyan + : Colors.white24, + width: 1.5, + ), + ), + child: Center( + child: Text( + initials, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + ), + ), + const SizedBox(width: 14), + + // Name & Verified + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + video.teacherName, + style: const TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w800, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 6), + const Icon( + CupertinoIcons.checkmark_seal_fill, + color: AppColors.saqelCyan, + size: 16, + ), + if (idx == 0) ...[ + const SizedBox(width: 6), + Container( + padding: + const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2), + decoration: BoxDecoration( + color: AppColors.guardianAmber + .withAlpha(30), + borderRadius: + BorderRadius.circular(6), + border: Border.all( + color: AppColors + .guardianAmber + .withAlpha(100)), + ), + child: const Text( + 'الأعلى تقييماً', + style: TextStyle( + color: AppColors.guardianAmber, + fontSize: 9.5, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ], + ), + const SizedBox(height: 4), + Row( + children: [ + const Icon( + CupertinoIcons.star_fill, + color: AppColors.guardianAmber, + size: 13, + ), + const SizedBox(width: 4), + Text( + isNew + ? 'حصة جديدة' + : '${video.rating.toStringAsFixed(1)} ★', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + if (!isNew) ...[ + const SizedBox(width: 6), + Text( + '(${video.ratingCount} تقييم موثق)', + style: const TextStyle( + color: + AppColors.textSecondaryDark, + fontSize: 11, + ), + ), + ], + const SizedBox(width: 10), + const Icon( + CupertinoIcons.checkmark_shield_fill, + color: AppColors.emeraldGreen, + size: 12, + ), + const SizedBox(width: 4), + const Text( + 'معتمد رسمياً', + style: TextStyle( + color: AppColors.textSecondaryDark, + fontSize: 11, + ), + ), + ], + ), + ], + ), + ), + + const Icon( + CupertinoIcons.chevron_left, + color: AppColors.saqelCyan, + size: 18, + ), + ], + ), + + const SizedBox(height: 10), + // Pedagogical approach badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: const Color(0xFF060B14), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.white12), + ), + child: Row( + children: [ + const Icon( + CupertinoIcons.lightbulb_fill, + color: AppColors.guardianAmber, + size: 12, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + approach, + style: const TextStyle( + color: AppColors.textSecondaryDark, + fontSize: 11, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + + const SizedBox(height: 12), + ], + ), + ), + ); + } +} diff --git a/apps/student_app/lib/presentation/screens/exams/adaptive_exam_screen.dart b/apps/student_app/lib/presentation/screens/exams/adaptive_exam_screen.dart index 35c37b7..25e0550 100644 --- a/apps/student_app/lib/presentation/screens/exams/adaptive_exam_screen.dart +++ b/apps/student_app/lib/presentation/screens/exams/adaptive_exam_screen.dart @@ -18,6 +18,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/utils/saqel_toast.dart'; +import '../../../data/models/exam_model.dart'; import '../../../logic/cubits/exam_cubit.dart'; import '../../widgets/luxury_widgets.dart'; @@ -26,18 +27,31 @@ class AdaptiveExamScreen extends StatelessWidget { final int examId; final String title; final String? subjectTitle; + final String? subjectCode; + final String? unitKey; + final ExamModel? initialExam; const AdaptiveExamScreen({ super.key, required this.examId, required this.title, this.subjectTitle, + this.subjectCode, + this.unitKey, + this.initialExam, }); @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => ExamCubit()..loadExam(examId: examId), + create: (context) => ExamCubit() + ..loadExam( + examId: examId, + initialExam: initialExam, + subjectCode: subjectCode, + unitKey: unitKey, + unitTitle: title, + ), child: _AdaptiveExamView(title: title, subjectTitle: subjectTitle), ); } diff --git a/apps/student_app/lib/presentation/screens/home/unified_home_screen.dart b/apps/student_app/lib/presentation/screens/home/unified_home_screen.dart index 79c2292..a3b0cb6 100644 --- a/apps/student_app/lib/presentation/screens/home/unified_home_screen.dart +++ b/apps/student_app/lib/presentation/screens/home/unified_home_screen.dart @@ -935,9 +935,29 @@ class _UnifiedHomeScreenState extends State { const Divider(color: AppColors.darkCardBorder), const SizedBox(height: 10), _buildGuardianStat('الامتحانات المنجزة بنجاح', '${selectedChild.examsPassed} من ${selectedChild.examsTotal}'), - _buildGuardianStat('دفتر الأخطاء الذكي', '8 من أصل 10 فجوات تم شفاؤها وإتقانها 🏆'), - _buildGuardianStat('نسبة الحضور ومشاهدة الحصص', '96% (28 حصة مكتملة)'), - _buildGuardianStat('فحوصات الفهم التفاعلية', '18 فحصاً مجتازاً بنجاح ✨'), + InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => SmartErrorNotebookScreen( + isGuardianMode: true, + studentName: selectedChild.name, + studentId: selectedChild.id, + ), + ), + ); + }, + borderRadius: BorderRadius.circular(10), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 4), + child: _buildGuardianStat( + 'دفتر الأخطاء والشفاء المعرفي 🔍 (اضغط للاطلاع)', + selectedChild.errorTotalCount > 0 + ? '${selectedChild.errorMasteredCount} من أصل ${selectedChild.errorTotalCount} فجوة تم شفاؤها (${selectedChild.errorMasteryRate.toStringAsFixed(0)}%) 🏆' + : 'سجل الطالب متقن — لا توجد فجوات غير معالجة ✨', + ), + ), + ), _buildGuardianStat('الرقم الوطني المشفر', selectedChild.nationalId ?? 'مسجل بالهاتف'), ], ), @@ -954,14 +974,13 @@ class _UnifiedHomeScreenState extends State { child: CupertinoAlertDialog( title: const Text('ومضة التقرير الشهري لولي الأمر 📲'), content: Text( - '🇯🇴 تقرير التحصيل الأكاديمي لشهر آب/أيلول 2026\n' + '🇯🇴 تقرير التحصيل الأكاديمي المعتمد\n' 'الطالب: ${selectedChild.name}\n' - 'المدرسة: ${selectedChild.schoolName ?? "الثقافة العسكرية"}\n\n' + 'المدرسة: ${selectedChild.schoolName ?? "مدرسة معتمدة"}\n\n' '• مؤشر الجاهزية للتوجيهي: ${selectedChild.readinessScore.toStringAsFixed(1)}%\n' - '• الحصص المكتملة: 28 حصة بنسبة التزام 96%\n' - '• دفتر الأخطاء: تم إتقان 8 فجوات بنجاح\n' - '• نتيجة الامتحان الموحد الأخير: 88%\n\n' - 'تم إرسال هذا التقرير عبر بوابة نبيه للواتساب برقم هاتف ولي الأمر.', + '• الامتحانات المنجزة: ${selectedChild.examsPassed} من ${selectedChild.examsTotal}\n' + '• دفتر الأخطاء والشفاء: ${selectedChild.errorMasteredCount} من ${selectedChild.errorTotalCount} فجوة تم شفاؤها\n\n' + 'تم إرسال هذا التقرير الموثق عبر بوابة نبيه للواتساب لهاتف ولي الأمر.', textAlign: TextAlign.start, ), actions: [ diff --git a/apps/student_app/lib/presentation/screens/notebook/smart_error_notebook_screen.dart b/apps/student_app/lib/presentation/screens/notebook/smart_error_notebook_screen.dart index 3f9d375..1dabf47 100644 --- a/apps/student_app/lib/presentation/screens/notebook/smart_error_notebook_screen.dart +++ b/apps/student_app/lib/presentation/screens/notebook/smart_error_notebook_screen.dart @@ -7,7 +7,16 @@ import '../../../data/repositories/error_notebook_repository.dart'; /// SAQEL ENTERPRISE - SMART ERROR NOTEBOOK & ADAPTIVE REMEDIATION SCREEN /// ============================================================================== class SmartErrorNotebookScreen extends StatefulWidget { - const SmartErrorNotebookScreen({super.key}); + final bool isGuardianMode; + final String? studentName; + final int? studentId; + + const SmartErrorNotebookScreen({ + super.key, + this.isGuardianMode = false, + this.studentName, + this.studentId, + }); @override State createState() => @@ -33,7 +42,9 @@ class _SmartErrorNotebookScreenState extends State { Future _loadNotebookData() async { setState(() => _isLoading = true); - final data = await _repository.getErrorNotebook(); + final data = (widget.isGuardianMode && widget.studentId != null) + ? await _repository.getChildErrorNotebook(widget.studentId!) + : await _repository.getErrorNotebook(); if (mounted) { setState(() { _summary = data['summary'] as ErrorNotebookSummary?; @@ -110,19 +121,43 @@ class _SmartErrorNotebookScreenState extends State { icon: const Icon(CupertinoIcons.back, color: Colors.white), onPressed: () => Navigator.of(context).pop(), ), - title: const Row( + title: Row( children: [ - Icon(CupertinoIcons.book_circle_fill, + const Icon(CupertinoIcons.book_circle_fill, color: Color(0xFF38BDF8), size: 22), - SizedBox(width: 8), - Text( - 'دفتر الأخطاء الذكي والمسار العلاجي', - style: TextStyle( - fontSize: 16.5, - fontWeight: FontWeight.w800, - color: Colors.white, + const SizedBox(width: 8), + Expanded( + child: Text( + widget.isGuardianMode + ? 'دفتر أخطاء الطالب: ${widget.studentName ?? "المسجل"}' + : 'دفتر الأخطاء الذكي والمسار العلاجي', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w800, + color: Colors.white, + ), + overflow: TextOverflow.ellipsis, ), ), + if (widget.isGuardianMode) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: const Color(0xFFF59E0B).withOpacity(0.2), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: const Color(0xFFF59E0B).withOpacity(0.5)), + ), + child: const Text( + 'رقابة الأهل 👨‍👧‍👦', + style: TextStyle( + color: Color(0xFFFBBF24), + fontSize: 10.5, + fontWeight: FontWeight.w700, + ), + ), + ), + ], ], ), actions: [ @@ -593,22 +628,46 @@ class _SmartErrorNotebookScreenState extends State { // Action Button if (!item.isMastered) - ElevatedButton.icon( - onPressed: () => _startRemediation(item), - icon: const Icon(CupertinoIcons.bolt_horizontal_circle_fill, size: 17), - label: const Text( - 'بدء المسار العلاجي لسد هذه الفجوة (3 أسئلة تفريدية) 🚀', - style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.w800), - ), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF0284C7), - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 42), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ) + widget.isGuardianMode + ? Container( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), + decoration: BoxDecoration( + color: const Color(0xFF0284C7).withOpacity(0.12), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFF0284C7).withOpacity(0.3)), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(CupertinoIcons.lock_shield, color: Color(0xFF38BDF8), size: 16), + SizedBox(width: 8), + Text( + 'مسار علاجي تفريدي ينجزه الطالب في حسابه لسد الفجوة 🎯', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: Color(0xFF38BDF8), + ), + ), + ], + ), + ) + : ElevatedButton.icon( + onPressed: () => _startRemediation(item), + icon: const Icon(CupertinoIcons.bolt_horizontal_circle_fill, size: 17), + label: const Text( + 'بدء المسار العلاجي لسد هذه الفجوة (3 أسئلة تفريدية) 🚀', + style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.w800), + ), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0284C7), + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 42), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ) else Container( padding: const EdgeInsets.symmetric(vertical: 8), diff --git a/apps/student_app/lib/presentation/screens/player/scaffolded_thinking_pause_sheet.dart b/apps/student_app/lib/presentation/screens/player/scaffolded_thinking_pause_sheet.dart new file mode 100644 index 0000000..f19d482 --- /dev/null +++ b/apps/student_app/lib/presentation/screens/player/scaffolded_thinking_pause_sheet.dart @@ -0,0 +1,688 @@ +import 'dart:async'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import '../../../core/theme/app_colors.dart'; +import '../../../data/models/subject_model.dart'; +import '../../widgets/luxury_widgets.dart'; + +/// Modal bottom sheet implementing the 60-second Thinking Pause and scaffolded step-by-step solution builder. +/// Pedagogy: Enforces student mental construction before revealing steps, eliminating passive learning. +class ScaffoldedThinkingPauseSheet extends StatefulWidget { + final CurriculumLessonItemModel lesson; + final SubjectModel? subject; + final VoidCallback? onCompleted; + + const ScaffoldedThinkingPauseSheet({ + super.key, + required this.lesson, + this.subject, + this.onCompleted, + }); + + static Future show( + BuildContext context, { + required CurriculumLessonItemModel lesson, + SubjectModel? subject, + VoidCallback? onCompleted, + }) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => ScaffoldedThinkingPauseSheet( + lesson: lesson, + subject: subject, + onCompleted: onCompleted, + ), + ); + } + + @override + State createState() => + _ScaffoldedThinkingPauseSheetState(); +} + +class _ScaffoldedThinkingPauseSheetState + extends State { + int _secondsRemaining = 60; + Timer? _countdownTimer; + bool _thinkingPhaseActive = true; + int _revealedStep = 0; // 0: None, 1: Step 1, 2: Step 2, 3: Step 3, 4: Complete + + @override + void initState() { + super.initState(); + _startTimer(); + } + + void _startTimer() { + _countdownTimer?.cancel(); + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (!mounted) return; + if (_secondsRemaining > 0) { + setState(() => _secondsRemaining--); + } else { + timer.cancel(); + setState(() { + _thinkingPhaseActive = false; + if (_revealedStep == 0) _revealedStep = 1; + }); + } + }); + } + + void _skipThinkingPhase() { + _countdownTimer?.cancel(); + setState(() { + _secondsRemaining = 0; + _thinkingPhaseActive = false; + if (_revealedStep == 0) _revealedStep = 1; + }); + } + + void _revealNextStep() { + if (_revealedStep < 4) { + setState(() => _revealedStep++); + if (_revealedStep == 4) { + widget.onCompleted?.call(); + } + } + } + + @override + void dispose() { + _countdownTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isMath = (widget.subject?.id ?? '').contains('math') || + widget.lesson.title.contains('معادل') || + widget.lesson.title.contains('رياضيات'); + final isPhysics = (widget.subject?.id ?? '').contains('physic') || + widget.lesson.title.contains('حركة') || + widget.lesson.title.contains('متجه'); + + return Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.88, + ), + decoration: const BoxDecoration( + color: AppColors.darkBackground, + borderRadius: BorderRadius.vertical(top: Radius.circular(26)), + border: Border( + top: BorderSide(color: AppColors.saqelCyan, width: 1.8), + ), + ), + child: SafeArea( + top: false, + child: Column( + children: [ + // Drag Handle + Container( + margin: const EdgeInsets.only(top: 12, bottom: 8), + width: 44, + height: 5, + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: BorderRadius.circular(3), + ), + ), + + // Top Header Bar + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + child: Row( + children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: AppColors.guardianAmber.withAlpha(30), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + CupertinoIcons.timer, + color: AppColors.guardianAmber, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'وقفة تفكير وبناء الحل خطوة بخطوة ⏱️', + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 2), + Text( + widget.lesson.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.textSecondaryDark, + fontSize: 11.5, + ), + ), + ], + ), + ), + IconButton( + icon: const Icon(CupertinoIcons.xmark_circle_fill, + color: Colors.white38, size: 24), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + + const Divider(color: AppColors.darkCardBorder, height: 1), + + // Scrollable Content + Expanded( + child: ListView( + padding: const EdgeInsets.all(20), + children: [ + // Problem statement box + _buildProblemCard(isMath, isPhysics), + + const SizedBox(height: 18), + + // 60-second Thinking Timer or Step Construction view + if (_thinkingPhaseActive) + _buildThinkingTimerCard() + else + _buildStepByStepSolutionCard(isMath, isPhysics), + ], + ), + ), + + // Bottom Action Bar + _buildBottomActionBar(), + ], + ), + ), + ); + } + + Widget _buildProblemCard(bool isMath, bool isPhysics) { + String title; + String problemBody; + String subHint; + + if (isMath) { + title = 'المسألة النموذجية الوزارية (حل نظام معادلتين):'; + problemBody = 'المعادلة (1) الخطية: y - x = 1\n' + 'المعادلة (2) التربيعية: x² + y² = 13\n\n' + 'المطلوب: جد مجموعة حل النظام بيانياً وجبرياً.'; + subHint = 'تلميح ذهني: ابدأ بعزل المتغير y في المعادلة الخطية أولاً، ثم عوضه في التربيعية.'; + } else if (isPhysics) { + title = 'المسألة النموذجية الوزارية (حركة المقذوفات في بعدين):'; + problemBody = 'أُطلقت قذيفة بسرعة ابتدائية v₀ = 50 m/s وبزاوية θ = 37° مع الأفق.\n' + 'بإهمال مقاومة الهواء واعتبار التسارع g = 10 m/s²:\n\n' + 'المطلوب: جد المركبتين الأفقية والعمودية للسرعة، وأقصى ارتفاع تصل إليه القذيفة.'; + subHint = 'تلميح ذهني: حلل السرعة إلى vx و vy أولاً، وتذكر أن السرعة العمودية عند أقصى ارتفاع تساوي صفراً.'; + } else { + title = 'السؤال النموذجي التطبيقي للدرس:'; + problemBody = 'حلل مفهوم «${widget.lesson.title}» وفق القواعد والمعايير المعتمدة في المنهاج الوزاري.\n\n' + 'المطلوب: صغ خطوات الإثبات والتحليل مع تقديم الدليل والمثال التوضيحي.'; + subHint = 'تلميح ذهني: اربط القاعدة بأركانها الأساسية ونفذ شروط التحقق المنهجي.'; + } + + return LuxuryCard( + borderColor: AppColors.saqelCyan.withAlpha(70), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(CupertinoIcons.doc_text_search, + color: AppColors.saqelCyan, size: 18), + const SizedBox(width: 8), + Expanded( + child: Text( + title, + style: const TextStyle( + color: AppColors.saqelCyan, + fontSize: 13.5, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + const SizedBox(height: 10), + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFF07101E), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white12), + ), + child: Text( + problemBody, + style: const TextStyle( + color: Colors.white, + fontSize: 13.5, + height: 1.55, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(height: 8), + Text( + subHint, + style: const TextStyle( + color: AppColors.guardianAmber, + fontSize: 11.5, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } + + Widget _buildThinkingTimerCard() { + final progress = (60 - _secondsRemaining) / 60.0; + + return LuxuryCard( + borderColor: AppColors.guardianAmber.withAlpha(90), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + // Circular countdown display + Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: 96, + height: 96, + child: CircularProgressIndicator( + value: progress, + strokeWidth: 6, + backgroundColor: Colors.white12, + valueColor: const AlwaysStoppedAnimation( + AppColors.guardianAmber), + ), + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '$_secondsRemaining', + style: const TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.w900, + fontFamily: 'SF Pro Text', + ), + ), + const Text( + 'ثانية', + style: TextStyle( + color: AppColors.textSecondaryDark, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 16), + const Text( + 'وقفة تأمل ذهني مستقل (60 ثانية) 🧠', + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 8), + const Text( + 'لا تبدأ بكتابة الحل فوراً! تدرب على استراتيجية الفهم أولاً:\n' + '• ما المتغيرات المتاحة وما نوع كل معادلة؟\n' + '• أي طرف هو الأيسر للعزل الرياضي؟\n' + '• توقع عدد الحلول الممكنة هندسياً قبل الحساب.', + textAlign: TextAlign.center, + style: TextStyle( + color: AppColors.textSecondaryDark, + fontSize: 12.5, + height: 1.55, + ), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + icon: const Icon(CupertinoIcons.play_circle_fill, size: 18), + label: const Text('أنا جاهز، ابدأ بناء الحل خطوة بخطوة 🚀'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.appleBlue, + foregroundColor: Colors.white, + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14)), + elevation: 4, + ), + onPressed: _skipThinkingPhase, + ), + ], + ), + ), + ); + } + + Widget _buildStepByStepSolutionCard(bool isMath, bool isPhysics) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Expanded( + child: Text( + 'بناء الحل التراكمي خطوة بخطوة 📐', + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w800, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: AppColors.saqelCyan.withAlpha(25), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColors.saqelCyan.withAlpha(60)), + ), + child: Text( + 'الخطوة $_revealedStep من 4', + style: const TextStyle( + color: AppColors.saqelCyan, + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + const SizedBox(height: 14), + + // Step 1 + _buildStepTile( + stepNum: 1, + title: isMath + ? 'الخطوة 1: عزل المتغير في المعادلة الخطية' + : (isPhysics + ? 'الخطوة 1: تحليل السرعة الابتدائية إلى مركبتين' + : 'الخطوة 1: تحديد المعطيات وضبط الأركان'), + detail: isMath + ? 'من المعادلة (1): y - x = 1 ⟹ y = x + 1\nتم جعل y موضوعاً للقانون لتسهيل التعويض.' + : (isPhysics + ? 'v₀x = v₀ cos(37°) = 50 × 0.8 = 40 m/s (سرعة أفقية ثابتة)\n' + 'v₀y = v₀ sin(37°) = 50 × 0.6 = 30 m/s (سرعة رأسية ابتدائية)' + : 'حصر المتغيرات والشروط الحاكمة للدرس وفق المنهج الوزاري.'), + isRevealed: _revealedStep >= 1, + ), + + const SizedBox(height: 12), + + // Step 2 + _buildStepTile( + stepNum: 2, + title: isMath + ? 'الخطوة 2: التعويض في المعادلة التربيعية وتصفيرها' + : (isPhysics + ? 'الخطوة 2: تطبيق معادلة الحركة الرأسية عند الذروة' + : 'الخطوة 2: تطبيق القاعدة المنهجية المباشرة'), + detail: isMath + ? 'نعوض y = x + 1 في المعادلة (2):\nx² + (x + 1)² = 13\nx² + (x² + 2x + 1) = 13\n2x² + 2x + 1 - 13 = 0 ⟹ 2x² + 2x - 12 = 0\nبالقسمة على 2: x² + x - 6 = 0' + : (isPhysics + ? 'عند أقصى ارتفاع: vy = 0\n' + 'نطبق: vy² = v₀y² - 2g(h_max)\n' + '0 = (30)² - 2(10)(h_max)\n' + '0 = 900 - 20(h_max) ⟹ 20(h_max) = 900 ⟹ h_max = 45 m' + : 'إجراء المقارنة النحوية أو الاستنتاج العلمي بناءً على الأركان السابقة.'), + isRevealed: _revealedStep >= 2, + ), + + const SizedBox(height: 12), + + // Step 3 + _buildStepTile( + stepNum: 3, + title: isMath + ? 'الخطوة 3: حساب المميز والتحليل إلى العوامل' + : (isPhysics + ? 'الخطوة 3: حساب زمن الصعود وزمن التحليق الكلي' + : 'الخطوة 3: استخراج الحكم المنهجي النهائي'), + detail: isMath + ? 'المميز الجبري: Δ = b² - 4ac = (1)² - 4(1)(-6) = 1 + 24 = 25 > 0 (يوجد حلان حقيقيان)\n' + 'تحليل العبارة التربيعية:\n(x + 3)(x - 2) = 0\n' + 'إما x = 2 أو x = -3' + : (isPhysics + ? 'vy = v₀y - gt ⟹ 0 = 30 - 10t_up ⟹ t_up = 3 s\n' + 'زمن التحليق الكلي: T_total = 2 × t_up = 2 × 3 = 6 s' + : 'التأكد من خلو الحل من التناقضات وتوافق الشروط الوزارية.'), + isRevealed: _revealedStep >= 3, + ), + + const SizedBox(height: 12), + + // Step 4 + _buildStepTile( + stepNum: 4, + title: isMath + ? 'الخطوة 4: إيجاد الأزواج المرتبة والتحقق البياني' + : (isPhysics + ? 'الخطوة 4: حساب المدى الأفقي الكلي والتحقق' + : 'الخطوة 4: توثيق النتيجة ونموذج الإجابة النموذجية'), + detail: isMath + ? 'عند x = 2: y = 2 + 1 = 3 ⟹ النقطة (2, 3)\n' + 'عند x = -3: y = -3 + 1 = -2 ⟹ النقطة (-3, -2)\n\n' + 'مجموعة حل النظام: {(2, 3), (-3, -2)}\n' + 'التحقق: 2² + 3² = 4 + 9 = 13 ✔ (تم التحقق جبرياً وهندسياً بنجاح)' + : (isPhysics + ? 'المدى الأفقي: R = vx × T_total = 40 × 6 = 240 m\n' + 'القذيفة تقطع 240 متراً أفقياً وتصل ارتفاعاً قدره 45 متراً ✔' + : 'تم صياغة الجواب النموذجي المعتمد بالكامل مع الإثبات الوزاري ✔'), + isRevealed: _revealedStep >= 4, + ), + ], + ); + } + + Widget _buildStepTile({ + required int stepNum, + required String title, + required String detail, + required bool isRevealed, + }) { + if (!isRevealed) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.white10), + ), + child: Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: Colors.white12, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + '$stepNum', + style: const TextStyle( + color: Colors.white38, + fontSize: 12, + fontWeight: FontWeight.w700), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'الخطوة $stepNum: قيد الإنشاء الذهني (اضغط لإظهار الخطوة)', + style: const TextStyle( + color: Colors.white38, + fontSize: 12, + fontWeight: FontWeight.w600), + ), + ), + const Icon(CupertinoIcons.lock_fill, + color: Colors.white24, size: 16), + ], + ), + ); + } + + return LuxuryCard( + borderColor: AppColors.saqelCyan.withAlpha(90), + child: Padding( + padding: const EdgeInsets.all(14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: AppColors.saqelCyan.withAlpha(30), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppColors.saqelCyan), + ), + child: Center( + child: Text( + '$stepNum', + style: const TextStyle( + color: AppColors.saqelCyan, + fontSize: 13, + fontWeight: FontWeight.w800), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.w700, + ), + ), + ), + const Icon(CupertinoIcons.checkmark_circle_fill, + color: AppColors.emeraldGreen, size: 18), + ], + ), + const SizedBox(height: 10), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFF07101E), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.white12), + ), + child: Text( + detail, + style: const TextStyle( + color: Colors.white70, + fontSize: 12.5, + height: 1.55, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildBottomActionBar() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + decoration: const BoxDecoration( + color: AppColors.darkSurface, + border: Border(top: BorderSide(color: AppColors.darkCardBorder)), + ), + child: Row( + children: [ + if (_thinkingPhaseActive) ...[ + Expanded( + child: OutlinedButton( + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.white24), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + onPressed: () => Navigator.of(context).pop(), + child: const Text('إغلاق والعودة للفيديو', + style: TextStyle(color: Colors.white70, fontSize: 13)), + ), + ), + ] else if (_revealedStep < 4) ...[ + Expanded( + child: ElevatedButton.icon( + icon: const Icon(CupertinoIcons.arrow_down_circle_fill, + size: 18), + label: Text( + 'اكشف الخطوة التالية (${_revealedStep + 1} من 4) ⬇️'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.saqelCyan, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + elevation: 2, + ), + onPressed: _revealNextStep, + ), + ), + ] else ...[ + Expanded( + child: ElevatedButton.icon( + icon: const Icon(CupertinoIcons.checkmark_seal_fill, size: 18), + label: const Text('اكتمل بناء الحل المنهجي بنجاح! استمر 🎯'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.emeraldGreen, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + elevation: 2, + ), + onPressed: () => Navigator.of(context).pop(), + ), + ), + ], + ], + ), + ); + } +} diff --git a/apps/student_app/lib/presentation/screens/player/socratic_video_player_screen.dart b/apps/student_app/lib/presentation/screens/player/socratic_video_player_screen.dart index a5c3d4d..6c1f2d2 100644 --- a/apps/student_app/lib/presentation/screens/player/socratic_video_player_screen.dart +++ b/apps/student_app/lib/presentation/screens/player/socratic_video_player_screen.dart @@ -29,6 +29,7 @@ import '../../widgets/socratic_dialog.dart'; import 'package:video_player/video_player.dart'; import 'package:flutter_tts/flutter_tts.dart'; import '../curriculum/math_interactive_lab_view.dart'; +import 'scaffolded_thinking_pause_sheet.dart'; /// مشغل الفيديو السقراطي الذكي ونقاط الفحص والإرجاع العلاجي class SocraticVideoPlayerScreen extends StatefulWidget { @@ -717,6 +718,19 @@ class _SocraticVideoPlayerScreenState extends State w ), ), const Spacer(), + IconButton( + tooltip: 'وقفة تفكير وبناء الحل خطوة بخطوة', + icon: const Icon(CupertinoIcons.timer, color: AppColors.guardianAmber, size: 20), + onPressed: () { + context.read().pause(); + ScaffoldedThinkingPauseSheet.show( + context, + lesson: widget.lesson, + subject: widget.subject, + ); + }, + ), + const SizedBox(width: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( @@ -844,6 +858,51 @@ class _SocraticVideoPlayerScreenState extends State w ], ), ), + // Thinking Pause & Scaffolded Solution Construction Card + GestureDetector( + onTap: () { + context.read().pause(); + ScaffoldedThinkingPauseSheet.show( + context, + lesson: widget.lesson, + subject: widget.subject, + ); + }, + child: LuxuryCard( + borderColor: AppColors.guardianAmber.withAlpha(90), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: AppColors.guardianAmber.withAlpha(25), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon(CupertinoIcons.timer, color: AppColors.guardianAmber, size: 24), + ), + const SizedBox(width: 14), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'وقفة تفكير ذهني وبناء الحل خطوة بخطوة ⏱️', + style: TextStyle(color: Colors.white, fontSize: 13.5, fontWeight: FontWeight.w800), + ), + SizedBox(height: 3), + Text( + 'تأمل المسألة لمدة 60 ثانية قبل كشف خطوات الإثبات والحل المنهجي.', + style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11.5), + ), + ], + ), + ), + const Icon(CupertinoIcons.chevron_left, color: AppColors.guardianAmber, size: 18), + ], + ), + ), + ), const SizedBox(height: 16), // Interactive Digital Chalkboard & Key Concept Breakdown @@ -1025,6 +1084,23 @@ class _SocraticVideoPlayerScreenState extends State w ); }, ), + OutlinedButton.icon( + icon: const Icon(CupertinoIcons.timer, size: 16, color: AppColors.guardianAmber), + label: const Text('وقفة تفكير وبناء الحل (60 ثانية) ⏱️', style: TextStyle(color: AppColors.guardianAmber, fontSize: 12, fontWeight: FontWeight.w700)), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: AppColors.guardianAmber), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: () { + context.read().pause(); + ScaffoldedThinkingPauseSheet.show( + context, + lesson: widget.lesson, + subject: widget.subject, + ); + }, + ), if (isMath) OutlinedButton.icon( icon: const Icon(CupertinoIcons.function, size: 16, color: AppColors.saqelCyan), diff --git a/apps/student_app/lib/presentation/screens/virtual_labs/arabic_unit2_insha_lab.dart b/apps/student_app/lib/presentation/screens/virtual_labs/arabic_unit2_insha_lab.dart new file mode 100644 index 0000000..0c05b98 --- /dev/null +++ b/apps/student_app/lib/presentation/screens/virtual_labs/arabic_unit2_insha_lab.dart @@ -0,0 +1,665 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'lab_identity.dart'; +import 'lab_scaffold.dart'; + +/// ============================================================================ +/// ARABIC — BUILD MY LANGUAGE (2): THE REQUESTIVE CONSTRUCTION (أسلوبُ +/// الإنشاءِ الطّلبيِّ — الإنشاءُ الطلبيُّ). +/// Lesson: الدرس السادس — أبني لغتي (2): الأسلوبُ الإنشائيّ (الإنشاءُ +/// الطلبيُّ). Source: grade_10/arabic_10/semester_1/unit_02/lesson_06.md +/// (pages 56-59). +/// ---------------------------------------------------------------------------- +/// Built ONLY on extractable facts from the source pages: +/// - مفهومُ الإنشاءِ الطّلبيِّ (ص56-57): كلامٌ لا يحتملُ التّصديقَ أو +/// التّكذيبَ؛ يرادُ بهِ طلبُ حصولِ أمرٍ لم يتحقَّقْ وقتَ الطّلبِ. +/// - الإنشاءُ الطلبيُّ يطلبُ حصولَ شيءٍ لم يقعْ بَعْدُ؛ ولا يُحكَمُ عليهِ +/// بصحّةٍ أو بطلانٍ. +/// - أنواعُ الإنشاءِ الطّلبيِّ الستّةُ (ص56-57): النداءُ، والأمرُ، +/// والنهيُ، والاستفهامُ، والتّمنّي، والتّرجّي. +/// - أدواتُه (ص56-57): حروفُ النداءِ (يا وأخواتُها) للنداءِ؛ فعلُ الأمرِ +/// أو المضارعُ المقترنُ بلامِ الأمرِ للأمرِ؛ (لا) النّاهيةُ للنهيِ؛ +/// أدواتُ الاستفهامِ (هل/كيف/متى/أين...) للاستفهامِ؛ (ليت) للتّمنّي؛ +/// (لعل) للتّرجّي. +/// - أمثلةُ الدرسِ (ص57-59): ﴿يا شُعيبُ أَصَلاتُكَ تَأْمُرُكَ...﴾ +/// (سورة هود 87) — نداءٌ + استفهامٌ؛ «ألا ليتَ شِعري هَل أبِيتُ +/// ليلةً...» (الفرزدقُ) — تمنٍّ + استفهامٌ؛ «يا أَيّها النّاسُ اتّقوا +/// ربَّكم» — نداءُ المعرَّفِ بـ(ال) بـ(أيّها)؛ «لا تَحسِبِ المَجْدَ +/// تَمْرًا أنتَ آكِلُهُ، لن تَبلُغَ المَجْدَ حتى تَلعَقَ الصَّبِرا» +/// (أبو العلاءِ المعرّي) — نهيٌ بـ(لا النّاهيةِ)؛ «السلامُ عليكم +/// دارَ قومٍ مؤمنينَ» — نداءٌ مقدَّرًا حرفُه (يا دارَ قومٍ)؛ +/// (اللّهُمَّ) عُوِّضَ حرفُ النداءِ فيهِ بميمٍ مشدّدةٍ مبدَلٍ منَ حرفِ +/// النداءِ المحذوفِ. +/// ============================================================================ + +// مفهومُ الإنشاءِ الطّلبيِّ (صواب/خطأ). +const List<(String, bool)> _requestStatements = [ + ( + 'الإنشاءُ الطلبيُّ كلامٌ لا يحتملُ التّصديقَ أو التّكذيبَ', + true + ), + ( + 'من صيغِ الإنشاءِ الطّلبيِّ: النداءُ، والأمرُ، والنهيُ، والاستفهامُ، والتّمنّي، والتّرجّي', + true + ), + ( + 'الأمرُ يكونُ بفعلِ الأمرِ أو بالمضارعِ المقترنِ بلامِ الأمرِ', + true + ), + ( + 'النهيُ يكونُ بـ(لا) النّاهيةِ قبلَ الفعلِ المضارعِ', + true + ), + ( + 'الاستفهامُ يكونُ بأدواتِ الاستفهامِ مثلَ: (هل، كيف، متى، أين)', + true + ), + ( + 'التّمنّي يكونُ بـ(ليت) والتّرجّي بـ(لعل)', + true + ), + ( + 'النداءُ يكونُ بحرفِ النداءِ وحدَهُ دونَ المنادى', + false + ), + ( + 'الإنشاءُ الطلبيُّ كلامٌ خبريٌّ يرادُ منهُ إخبارُ المخاطَبِ بشيءٍ وقعَ فعلًا', + false + ), +]; + +// أنواعُ الإنشاءِ الطّلبيِّ معَ مثالِهِ منَ الدرسِ. +const List<(String, String, String, int)> _typesRows = [ + ( + 'النداءُ', + 'حرفُ النداءِ (يا وأخواتُها) ثمّ المنادى.', + 'يا شُعيبُ أَصلاةُكَ تأمرُكَ...', + 0 + ), + ( + 'الأمرُ', + 'فعلُ الأمرِ أو المضارعُ بلامِ الأمرِ.', + 'اقرأْ باسمِ ربِّكَ الّذي خَلَقَ', + 1 + ), + ( + 'النهيُ', + '(لا) النّاهيةُ قبلَ المضارعِ.', + 'لا تَحسِبِ المَجْدَ تَمْرًا أنتَ آكِلُهُ', + 2 + ), + ( + 'الاستفهامُ', + 'أدواتُ الاستفهامِ (هل/كيف/متى/أين).', + 'هَل أبِيتُ ليلةً...', + 3 + ), + ( + 'التّمنّي', + '(ليت).', + 'ألا ليتَ شِعري هَل أبِيتُ...', + 4 + ), + ( + 'التّرجّي', + '(لعل).', + 'لعلَّ اللهَ يَفرِّجُ عنّي', + 5 + ), +]; + +// أمثلةُ توظيفِ الدرسِ (ص57-59) — اختيارُ النّوعِ الصّحيحِ. +const List<(String, String, int)> _situationalStatements = [ + ( + 'يا أَيّها النّاسُ اتّقوا ربَّكم', + 'النداءُ', + 0 + ), + ( + 'اقرأْ باسمِ ربِّكَ الّذي خَلَقَ', + 'الأمرُ', + 1 + ), + ( + 'لا تَحسِبِ المَجْدَ تَمْرًا أنتَ آكِلُهُ', + 'النهيُ', + 2 + ), + ( + 'هَل أبِيتُ ليلةً بِبَثنَةَ ليلةً', + 'الاستفهامُ', + 3 + ), + ( + 'ألا ليتَ شِعري', + 'التّمنّي', + 4 + ), + ( + 'لعلَّ اللهَ يَفرِّجُ عنّي', + 'التّرجّي', + 5 + ), +]; + +class ArabicUnit2InshaLabView extends StatefulWidget { + final LabCheckpointCallback? onCheckpointTriggered; + + const ArabicUnit2InshaLabView({super.key, this.onCheckpointTriggered}); + + @override + State createState() => + _ArabicUnit2InshaLabViewState(); +} + +class _ArabicUnit2InshaLabViewState extends State { + int _activity = 0; + + // Activity 0 — مفهومُ الإنشاءِ الطّلبيِّ. + final List _request = List.filled(_requestStatements.length, false); + bool _requestTouched = false; + + // Activity 1 — أدواتُ أنواعِ الإنشاءِ الطّلبيِّ. + final List _selected = List.filled(_typesRows.length, -1); + bool _typesTouched = false; + + // Activity 2 — توظيفُ الإنشاءِ الطّلبيِّ في أمثلةِ الدرسِ. + final List _situational = List.filled(_situationalStatements.length, -1); + bool _situationalTouched = false; + + bool get _requestDone { + for (var i = 0; i < _requestStatements.length; i++) { + if (_request[i] != _requestStatements[i].$2) return false; + } + return true; + } + + bool get _typesDone { + if (!_typesTouched) return false; + for (var i = 0; i < _typesRows.length; i++) { + if (_selected[i] != _typesRows[i].$4) return false; + } + return true; + } + + bool get _situationalDone { + if (!_situationalTouched) return false; + for (var i = 0; i < _situationalStatements.length; i++) { + if (_situational[i] != _situationalStatements[i].$3) return false; + } + return true; + } + + bool get _done { + if (_activity == 0) return _requestDone && _requestTouched; + if (_activity == 1) return _typesDone; + return _situationalDone; + } + + Widget _buildCanvas() { + switch (_activity) { + case 0: + return CustomPaint( + painter: _RequestConstructionPainter( + checks: List.of(_request), done: _requestDone), + size: Size.infinite, + ); + case 1: + return CustomPaint( + painter: _RequestTypesPainter( + selected: List.of(_selected), done: _typesDone), + size: Size.infinite, + ); + default: + return CustomPaint( + painter: _RequestSituationalPainter( + picks: List.of(_situational), done: _situationalDone), + size: Size.infinite, + ); + } + } + + List _buildControls() { + final c = [ + LabSegments( + labels: const ['مفهومُ الإنشاءِ', 'أنواعُ الإنشاءِ', 'توظيفُ الإنشاءِ'], + values: const [0, 1, 2], + current: _activity, + onSelected: (v) => setState(() { + _activity = v; + saqelTick(); + }), + ), + const SizedBox(height: 10), + ]; + + if (_activity == 0) { + c.addAll([ + const Text( + 'أُعيِّنُ (صوابًا أو خطأً) على جُمَلِ مفهومِ الإنشاءِ الطّلبيِّ وصيغِهِ.', + style: TextStyle(color: Colors.white, fontSize: 12.5, height: 1.6), + ), + const SizedBox(height: 10), + for (var i = 0; i < _requestStatements.length; i++) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: LabToggle( + label: _requestStatements[i].$1, + value: _request[i], + onChanged: (v) => setState(() { + _request[i] = v; + _requestTouched = true; + if (_requestDone) saqelTick(); + }), + ), + ), + if (_requestDone && _requestTouched) + const LabPill( + 'مفهومُ الإنشاءِ الطّلبيِّ مثبَّتٌ: كلامٌ لا يحتملُ التّصديقَ أو التّكذيبَ، يطلبُ حصولَ شيءٍ لم يقعْ بَعْدُ؛ والمرادُ: طلبُ حصولِ أمرٍ غيرِ متحقِّقٍ، لا إخبارٌ عمّا وقعْ ✓', + color: Color(0xFF30D158)) + else + const LabPill( + 'الصوابُ: الإنشاءُ الطلبيُّ كلامٌ لا يحتملُ التّصديقَ أو التّكذيبَ، يطلبُ حصولَ أمرٍ لم يتحقَّقْ وقتَ الطّلبِ (نداءٌ، أمرٌ، نهيٌ، استفهامٌ، تمنٍّ، ترجٍّ). الخطأُ: النداءُ بلا منادى، والخبريُّ الذي يرادُ بهِ إخبارُ المخاطَبِ بما وقعَ.', + color: Color(0xFFFF9F0A)), + ]); + } else if (_activity == 1) { + c.addAll([ + const Text( + 'أُقرِّرُ لكلِّ نوعٍ من أنواعِ الإنشاءِ الطّلبيِّ أسلوبَهُ الصّحيحَ من خلالِ مثالِ الدرسِ.', + style: TextStyle(color: Colors.white, fontSize: 12.5, height: 1.6), + ), + const SizedBox(height: 10), + for (var r = 0; r < _typesRows.length; r++) + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _typesRows[r].$1, + style: const TextStyle(color: Color(0xFF00F5D4), fontSize: 12.5), + ), + const SizedBox(height: 8), + Wrap( + spacing: 6, + runSpacing: 6, + children: [ + for (var t = 0; t < _typesRows.length; t++) + ChoiceChip( + label: Text( + _typesRows[t].$1, + style: const TextStyle(color: Colors.white, fontSize: 11), + ), + selected: _selected[r] == t, + onSelected: (v) => setState(() { + _selected[r] = v ? t : -1; + _typesTouched = true; + if (_typesDone) saqelTick(); + }), + selectedColor: const Color(0xFF00F5D4).withValues(alpha: 0.25), + backgroundColor: const Color(0x0DFFFFFF), + side: BorderSide( + color: _selected[r] == t + ? const Color(0xFF00F5D4) + : Colors.white12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + ], + ), + ], + ), + ), + if (_typesDone) + const LabPill( + 'أدواتُ أنواعِ الإنشاءِ الطّلبيِّ مثبّتةٌ: النداءُ (يا وأخواتُها)، الأمرُ (فعلُ الأمرِ/لامُ الأمرِ)، النهيُ (لا النّاهيةُ)، الاستفهامُ (هل/كيف/متى/أين)، التّمنّي (ليت)، التّرجّي (لعل) ✓', + color: Color(0xFF30D158)) + else + const LabPill( + 'راجع: النداءُ يُبنى على حرفِ النداءِ والمنادى؛ الأمرُ بفعلِ الأمرِ؛ النهيُ بلا النّاهيةِ؛ الاستفهامُ بأدواتِهِ؛ التّمنّي بليت؛ والتّرجّي بلعل.', + color: Color(0xFFFF9F0A)), + ]); + } else { + c.addAll([ + const Text( + 'أُحدِّدُ نوعَ الإنشاءِ الطّلبيِّ في كلِّ مثالٍ من أمثلةِ الدرسِ.', + style: TextStyle(color: Colors.white, fontSize: 12.5, height: 1.6), + ), + const SizedBox(height: 10), + for (var i = 0; i < _situationalStatements.length; i++) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + Expanded( + child: Text( + _situationalStatements[i].$1, + style: const TextStyle(color: Colors.white70, fontSize: 11), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 150, + child: DropdownButtonFormField( + initialValue: _situational[i] < 0 ? null : _situational[i], + dropdownColor: const Color(0xFF0B1728), + decoration: InputDecoration( + isDense: true, + filled: true, + fillColor: const Color(0x0DFFFFFF), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + ), + items: [ + for (var t = 0; t < _typesRows.length; t++) + DropdownMenuItem( + value: t, + child: Text( + _typesRows[t].$1, + style: const TextStyle( + color: Colors.white, fontSize: 11), + ), + ), + ], + onChanged: (v) => setState(() { + _situational[i] = v!; + _situationalTouched = true; + if (_situationalDone) saqelTick(); + }), + ), + ), + ], + ), + ), + if (_situationalDone) + const LabPill( + 'توظيفُ الإنشاءِ الطّلبيِّ سليمٌ في أمثلةِ الدرسِ كلِّها ✓', + color: Color(0xFF30D158)) + else + const LabPill( + 'الصوابُ: (يا أَيّها النّاسُ) نداءٌ، (اقرأْ) أمرٌ، (لا تَحسِبْ) نهيٌ، (هل) استفهامٌ، (ليت) تمنٍّ، (لعل) ترجٍّ.', + color: Color(0xFFFF9F0A)), + ]); + } + + if (_done) { + c.addAll([ + const SizedBox(height: 12), + LabCheckpointButton( + question: + 'لماذا لا يحتملُ الإنشاءُ الطلبيُّ التّصديقَ أو التّكذيبَ؟ وكيف نفرِّقُ بينَ الخبريِّ والإنشائيّ الطّلبيِّ؟', + options: const [ + 'لأنّهُ يطلبُ حصولَ أمرٍ لم يتحقَّقْ وقتَ الطّلبِ، فهوَ ليسَ خبرًا يُصحَّحُ أو يُكذَّبُ', + 'لأنّهُ خبرٌ صادقٌ يقبلُ التّصديقَ أو التّكذيبَ', + 'لأنّهُ طلبٌ لا يرتبطُ بحصولِ أمرٍ في المستقبلِ', + ], + correctIdx: 0, + onCheckpointTriggered: widget.onCheckpointTriggered, + ), + ]); + } + return c; + } + + @override + Widget build(BuildContext context) { + return SaqelLabScaffold( + identity: kArabicUnit2InshaLabIdentity, + titleAr: 'مختبرُ الأسلوبِ الإنشائيّ الطّلبيِّ', + subtitleAr: 'أبني لغتي (2) — الإنشاءُ الطلبيُّ: طلبُ حصولِ أمرٍ لم يقعْ بَعْدُ، لا يحتملُ التّصديقَ أو التّكذيبَ.', + canvas: _buildCanvas(), + controls: _buildControls(), + footerNote: + 'مصدر: صفحات 56-59. الإنشاءُ الطلبيُّ طلبُ أمرٍ غيرِ متحقِّقٍ وقتَ الطّلبِ، لا يحتملُ التّصديقَ أو التّكذيبَ، بخلافِ الخبريِّ. أنوعُه الستّةُ بأدواتِها وأمثلةُ الدرسِ (يا شُعيبُ، قرأْ، لا تَحسِبْ، هل، ليت، لعل) — قابلٌ للإثباتِ من ص56-59.', + checkpointQuestion: + 'لماذا لا يحتملُ الإنشاءُ الطلبيُّ التّصديقَ أو التّكذيبَ؟', + checkpointOptions: const [ + 'لأنّهُ يطلبُ حصولَ أمرٍ لم يتحقَّقْ وقتَ الطّلبِ، فهوَ ليسَ خبرًا يُصحَّحُ أو يُكذَّبُ.', + 'لأنّهُ خبرٌ صادقٌ يُقبلُ التّصديقَ أو التّكذيبَ.', + 'لأنّهُ طلبٌ لا يرتبطُ بأمرٍ يُطلبُ.', + ], + checkpointCorrectIdx: 0, + ); + } +} + +// ============================================================================ +// Canvas 0 — مفهومُ الإنشاءِ الطّلبيِّ. +// ============================================================================ +class _RequestConstructionPainter extends CustomPainter { + final List checks; + final bool done; + + const _RequestConstructionPainter({ + required this.checks, + required this.done, + }); + + @override + void paint(Canvas canvas, Size size) { + final w = size.width, h = size.height; + var lit = 0; + for (var i = 0; i < checks.length; i++) { + if (checks[i] == _requestStatements[i].$2) lit++; + } + final pct = _requestStatements.isEmpty ? 0.0 : lit / _requestStatements.length; + canvas.drawCircle( + Offset(w / 2, h * 0.22), + 30, + Paint()..color = const Color(0xFF0B1728), + ); + canvas.drawArc( + Rect.fromCircle(center: Offset(w / 2, h * 0.22), radius: 34), + -math.pi / 2, + math.pi * 2 * pct, + false, + Paint() + ..color = done ? const Color(0xFF30D158) : const Color(0xFF00F5D4) + ..style = PaintingStyle.stroke + ..strokeWidth = 3, + ); + _centerText( + canvas, + Offset(w / 2, h * 0.22), + done ? 'طلبٌ متحقِّقٌ ✓' : 'طلبُ حصولِ أمرٍ لم يقعْ بَعْدُ', + TextStyle( + color: done ? const Color(0xFF30D158) : Colors.white70, + fontSize: 9.5, + fontWeight: FontWeight.w700), + maxWidth: w * 0.4, + ); + + for (var i = 0; i < _requestStatements.length; i++) { + final y = h * 0.38 + i * (h * 0.55) / _requestStatements.length; + final on = checks[i] == _requestStatements[i].$2; + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromCenter( + center: Offset(w / 2, y), width: w * 0.82, height: 30), + const Radius.circular(15), + ), + Paint()..color = const Color(0xFF0B1728), + ); + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromCenter( + center: Offset(w / 2, y), width: w * 0.82, height: 30), + const Radius.circular(15), + ), + Paint() + ..color = on ? const Color(0xFF30D158) : Colors.white12 + ..style = PaintingStyle.stroke + ..strokeWidth = 1.2, + ); + _centerText( + canvas, + Offset(w / 2, y), + (on ? '✓ ' : '· ') + _requestStatements[i].$1, + TextStyle( + color: on ? const Color(0xFF30D158) : Colors.white70, + fontSize: 9.5), + maxWidth: w * 0.76, + ); + } + } + + @override + bool shouldRepaint(_RequestConstructionPainter old) => + old.checks != checks || old.done != done; +} + +// ============================================================================ +// Canvas 1 — أدواتُ أنواعِ الإنشاءِ الطّلبيِّ. +// ============================================================================ +class _RequestTypesPainter extends CustomPainter { + final List selected; + final bool done; + + const _RequestTypesPainter({required this.selected, required this.done}); + + @override + void paint(Canvas canvas, Size size) { + final w = size.width, h = size.height; + final center = Offset(w / 2, h * 0.5); + _centerText( + canvas, + Offset(w / 2, h * 0.12), + done ? 'عجلةُ الإنشاءِ الطّلبيِّ مضبوطةٌ ✓' : 'أنواعُ الإنشاءِ الطّلبيِّ', + TextStyle( + color: done ? const Color(0xFF30D158) : Colors.white70, + fontSize: 12, + fontWeight: FontWeight.w700), + maxWidth: w * 0.86, + ); + + canvas.drawCircle(center, 30, Paint()..color = const Color(0xFF0B1728)); + canvas.drawCircle( + center, + 90, + Paint() + ..color = done ? const Color(0xFF30D158) : Colors.white12 + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + ); + + for (var i = 0; i < _typesRows.length; i++) { + final a = -math.pi / 2 + i * 2 * math.pi / _typesRows.length; + final pos = center + Offset(math.cos(a), math.sin(a)) * 68; + final on = selected[i] == i; + canvas.drawCircle(pos, 15, Paint()..color = const Color(0xFF0B1728)); + canvas.drawCircle( + pos, + 15, + Paint() + ..color = on ? const Color(0xFF30D158) : Colors.white12 + ..style = PaintingStyle.stroke + ..strokeWidth = 1.2, + ); + _centerText( + canvas, + pos, + on ? '✓ ${_typesRows[i].$1}' : _typesRows[i].$1, + TextStyle( + color: on ? const Color(0xFF30D158) : Colors.white70, + fontSize: 10, + fontWeight: FontWeight.w700), + maxWidth: w * 0.3, + ); + final io = center + Offset(math.cos(a), math.sin(a)) * 68; + _centerText( + canvas, + io + const Offset(0, 28), + _typesRows[i].$2, + TextStyle( + color: done ? const Color(0xFF30D158) : Colors.white60, + fontSize: 7.5), + maxWidth: w * 0.26, + ); + } + } + + @override + bool shouldRepaint(_RequestTypesPainter old) => + old.selected != selected || old.done != done; +} + +// ============================================================================ +// Canvas 2 — توظيفُ أمثلةِ الدرسِ. +// ============================================================================ +class _RequestSituationalPainter extends CustomPainter { + final List picks; + final bool done; + + const _RequestSituationalPainter({required this.picks, required this.done}); + + @override + void paint(Canvas canvas, Size size) { + final w = size.width, h = size.height; + _centerText( + canvas, + Offset(w / 2, h * 0.10), + done ? 'توظيفُ الإنشاءِ الطّلبيِّ سليمٌ ✓' : 'أُحدِّدُ نوعَ الإنشاءِ في مثالٍ', + TextStyle( + color: done ? const Color(0xFF30D158) : Colors.white70, + fontSize: 12, + fontWeight: FontWeight.w700), + maxWidth: w * 0.9, + ); + + for (var i = 0; i < _situationalStatements.length; i++) { + final correct = _situationalStatements[i].$3; + final on = picks[i] == correct; + final y = h * 0.20 + i * (h * 0.72) / _situationalStatements.length; + final outline = RRect.fromRectAndRadius( + Rect.fromCenter( + center: Offset(w / 2, y), width: w * 0.86, height: 30), + const Radius.circular(15), + ); + canvas.drawRRect(outline, Paint()..color = const Color(0xFF0B1728)); + canvas.drawRRect( + outline, + Paint() + ..color = on ? const Color(0xFF30D158) : Colors.white12 + ..style = PaintingStyle.stroke + ..strokeWidth = 1.2, + ); + _centerText( + canvas, + Offset(w / 2, y), + (on ? '✓ ' : '· ') + _situationalStatements[i].$1, + TextStyle( + color: on ? const Color(0xFF30D158) : Colors.white70, + fontSize: 9.5), + maxWidth: w * 0.8, + ); + } + } + + @override + bool shouldRepaint(_RequestSituationalPainter old) => + old.picks != picks || old.done != done; +} + +// ============================================================================ +// مساعداتُ رسمٍ مشتركةٌ. +// ============================================================================ +void _centerText( + Canvas canvas, + Offset center, + String text, + TextStyle style, { + double? maxWidth, +}) { + final tp = TextPainter( + text: TextSpan(text: text, style: style), + textDirection: TextDirection.rtl, + textAlign: TextAlign.center, + maxLines: 4, + )..layout(maxWidth: maxWidth ?? double.infinity); + tp.paint(canvas, Offset(center.dx - tp.width / 2, center.dy - tp.height / 2)); +} diff --git a/apps/student_app/lib/presentation/screens/virtual_labs/earth_air_masses_lab.dart b/apps/student_app/lib/presentation/screens/virtual_labs/earth_air_masses_lab.dart new file mode 100644 index 0000000..6be1b12 --- /dev/null +++ b/apps/student_app/lib/presentation/screens/virtual_labs/earth_air_masses_lab.dart @@ -0,0 +1,561 @@ +import 'dart:math' as math; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import '../../../core/theme/app_colors.dart'; +import 'lab_identity.dart'; +import 'lab_scaffold.dart'; + +/// ============================================================================ +/// EARTH & ENVIRONMENTAL SCIENCES — GRADE 10 +/// Unit 3, Lesson 1 — الكتل والجبهات الهوائية (Air Masses & Fronts) +/// Grounded verbatim in Ministry textbook pages 8-15: +/// - 4 Air Masses: cP (قارية قطبية), mP (بحرية قطبية), cT (قارية مدارية), mT (بحرية مدارية) +/// - 4 Front Types: جبهة باردة (Cold), جبهة دافئة (Warm), جبهة مقفلة (Occluded), جبهة مستقرة (Stationary) +/// - Visual cloud formation, precipitation styles, and temperature boundary profile +/// ============================================================================ + +class EarthAirMassesLabView extends StatefulWidget { + final LabCheckpointCallback? onCheckpointTriggered; + const EarthAirMassesLabView({super.key, this.onCheckpointTriggered}); + + @override + State createState() => _EarthAirMassesLabViewState(); +} + +class _EarthAirMassesLabViewState extends State + with SingleTickerProviderStateMixin { + int _frontMode = 0; // 0 = Cold Front, 1 = Warm Front, 2 = Stationary, 3 = Occluded + int _selectedAirMass = 0; // 0 = cP, 1 = mP, 2 = cT, 3 = mT + late final AnimationController _cloudAnimController; + + static const List> _airMasses = [ + { + 'code': 'cP', + 'name': 'قارية قطبية (Continental Polar)', + 'temp': 'شديدة البرودة (-10°C إلى 2°C)', + 'humidity': 'جافة جداً (رطوبة منخفضة)', + 'source': 'سيبيريا وشمال كندا وأوراسيا', + 'jordanImpact': 'موجات صقيع وانجماد جافة شتاءً في الأردن', + 'color': Color(0xFF60A5FA), + }, + { + 'code': 'mP', + 'name': 'بحرية قطبية (Maritime Polar)', + 'temp': 'باردة ورطبة (2°C إلى 8°C)', + 'humidity': 'عالية الرطوبة والتشبع', + 'source': 'شمال المحيط الأطلسي والقطب الشمالي', + 'jordanImpact': 'منخفضات جوية شتوية مصحوبة بأمطار وثلوج غزيرة', + 'color': Color(0xFF38BDF8), + }, + { + 'code': 'cT', + 'name': 'قارية مدارية (Continental Tropical)', + 'temp': 'حارة جداً وجافة (34°C إلى 42°C)', + 'humidity': 'جافة ومغبرة أحياناً', + 'source': 'شبه الجزيرة العربية والصحراء الكبرى', + 'jordanImpact': 'موجات حر صيفية ورياح خماسينية في الربيع', + 'color': Color(0xFFF59E0B), + }, + { + 'code': 'mT', + 'name': 'بحرية مدارية (Maritime Tropical)', + 'temp': 'دافئة ورطبة (24°C إلى 30°C)', + 'humidity': 'رطوبة جوية مرتفعة وضباب', + 'source': 'المحيط الأطلسي والبحر الأحمر وخليج العقبة', + 'jordanImpact': 'حالات عدم استقرار جوي وزخات رعدية مفاجئة', + 'color': Color(0xFF10B981), + }, + ]; + + static const List> _frontTypes = [ + { + 'title': 'الجبهة الهوائية الباردة (Cold Front)', + 'symbol': 'مثلثات زرقاء تشير لاتجاه الحركة ▲▲▲', + 'mechanism': 'هواء بارد كثيف يندفع سريعاً تحت الهواء الدافئ الأقل كثافة، فيرفعه بقوة للأعلى.', + 'clouds': 'غيوم المزن الركامية (Cumulonimbus) الشاهقة', + 'weather': 'أمطار غزيرة مفاجئة، عواصف رعدية وزخات بَرَد ورياح نشطة يعقبها انخفاض ملموس في الحرارة.', + 'color': Color(0xFF2563EB), + }, + { + 'title': 'الجبهة الهوائية الدافئة (Warm Front)', + 'symbol': 'أنصاف دوائر حمراء تشير لاتجاه الحركة ●●●', + 'mechanism': 'هواء دافئ يصعد تدريجياً وببطء فوق كتلة هوائية باردة ثابتة أو بطيئة.', + 'clouds': 'غيوم طبقية (Stratus) تبدأ بالسمحاقية ثم الركامية المتوسطة فالطبقية المنبسطة', + 'weather': 'أمطار ديمية مستمرة وخفيفة إلى متوسطة على مساحات شاسعة، مع ارتفاع تدريجي في درجات الحرارة.', + 'color': Color(0xFFDC2626), + }, + { + 'title': 'الجبهة الهوائية المستقرة (Stationary Front)', + 'symbol': 'مثلثات زرقاء في جهة وأنصاف دوائر حمراء في الجهة المقابلة', + 'mechanism': 'تلتقي كتلة دافئة وأخرى باردة دون أن تتمكن إحداهما من إزاحة الأخرى لتعادل القوى.', + 'clouds': 'غيوم طبقية رمادية كثيفة', + 'weather': 'أجواء غائمة لعدة أيام مع هطول أمطار متقطعة ومستمرة في نفس المنطقة الجغرافية.', + 'color': Color(0xFF8B5CF6), + }, + { + 'title': 'الجبهة الهوائية المقفلة (Occluded Front)', + 'symbol': 'مثلثات وأنصاف دوائر بنفسجية متبادلة على نفس الخط', + 'mechanism': 'جبهة باردة سريعة الحركة تلحق بجبهة دافئة وترفع الهواء الدافئ عن سطح الأرض بالكامل.', + 'clouds': 'مزيج معقد من الغيوم الركامية والطبقية الكثيفة', + 'weather': 'أمطار معقدة وشديدة وتبريد حاد يليه استقرار وتلاشي تدريجي للمنخفض الجوي.', + 'color': Color(0xFF7C3AED), + }, + ]; + + @override + void initState() { + super.initState(); + _cloudAnimController = AnimationController( + vsync: this, + duration: const Duration(seconds: 4), + )..repeat(); + } + + @override + void dispose() { + _cloudAnimController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final activeAirMass = _airMasses[_selectedAirMass]; + final activeFront = _frontTypes[_frontMode]; + + return SaqelLabScaffold( + titleAr: 'الكتل والجبهات الهوائية — علوم الأرض', + subtitleAr: 'محاكاة ديناميكية لتصادم الكتل الهوائية وتشكل الغيوم وأنماط الهطول المعتمدة', + identity: const LabIdentity( + curriculumLessonId: 'earth_sciences_10_semester_2_unit_03_lesson_01', + subjectKey: 'earth_sciences_10', + semesterKey: 'semester_2', + unitKey: 'unit_03', + lessonKey: 'lesson_01', + sourceMarkdown: 'grade_10/earth_sciences_10/semester_2/unit_03/lesson_01.md', + subjectAr: 'علوم الأرض والبيئة', + lessonAr: 'الكتل والجبهات الهوائية', + ), + onCheckpointTriggered: widget.onCheckpointTriggered, + checkpointQuestion: + 'عندما يندفع هواء بارد كثيف سريعاً أسفل هواء دافئ رطب، تتشكل جبهة باردة تؤدي إلى …', + checkpointOptions: const [ + 'غيوم المزن الركامية الشاهقة وأمطار غزيرة وعواصف رعدية', + 'أجواء صافية وجافة تماماً دون أي غيوم', + 'ارتفاع مفاجئ في درجات الحرارة والرياح الخماسينية', + 'غيوم رقيقة جداً لا ينتج عنها أي هطول' + ], + checkpointCorrectIdx: 0, + telemetry: [ + LabPill('النوع: ${activeFront['title']!.split('(').first.trim()}', + color: activeFront['color'] as Color), + LabPill('الكتلة: ${activeAirMass['code']}', + color: activeAirMass['color'] as Color), + LabPill('الرمز: ${activeFront['symbol']!.split(' ').first}'), + ], + canvas: AnimatedBuilder( + animation: _cloudAnimController, + builder: (context, _) { + return CustomPaint( + painter: _AtmosphericFrontPainter( + frontMode: _frontMode, + airMassCode: activeAirMass['code'] as String, + airMassColor: activeAirMass['color'] as Color, + progress: _cloudAnimController.value, + ), + child: Container(), + ); + }, + ), + controls: [ + const Text( + 'اختر نمط الجبهة الهوائية للتصادم (Front Dynamics):', + style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + LabSegments( + labels: const ['جبهة باردة ❄️', 'جبهة دافئة ☀️', 'مستقرة ⏸️', 'مقفلة 🌀'], + values: const [0, 1, 2, 3], + current: _frontMode, + onSelected: (val) => setState(() => _frontMode = val), + ), + const SizedBox(height: 14), + + // Information card about current front + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFF0B1424), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: (activeFront['color'] as Color).withAlpha(90)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(CupertinoIcons.wind, color: activeFront['color'] as Color, size: 16), + const SizedBox(width: 8), + Expanded( + child: Text( + activeFront['title'] as String, + style: TextStyle( + color: activeFront['color'] as Color, + fontSize: 13, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + const SizedBox(height: 6), + Text( + 'آلية التكون: ${activeFront['mechanism']}', + style: const TextStyle(color: Colors.white70, fontSize: 11.5, height: 1.45), + ), + const SizedBox(height: 4), + Text( + 'نوع الغيوم الناتجة: ${activeFront['clouds']}', + style: const TextStyle(color: AppColors.saqelCyan, fontSize: 11.5, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 4), + Text( + 'الطقس المصاحب: ${activeFront['weather']}', + style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11), + ), + ], + ), + ), + + const SizedBox(height: 16), + const Text( + 'فحص تصنيف الكتل الهوائية المؤثرة على الأردن (Air Masses):', + style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + LabSegments( + labels: const ['cP قارية قطبية', 'mP بحرية قطبية', 'cT قارية مدارية', 'mT بحرية مدارية'], + values: const [0, 1, 2, 3], + current: _selectedAirMass, + onSelected: (val) => setState(() => _selectedAirMass = val), + ), + const SizedBox(height: 10), + + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFF07101E), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${activeAirMass['code']}: ${activeAirMass['name']}', + style: TextStyle( + color: activeAirMass['color'] as Color, + fontSize: 12.5, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'الحرارة والرطوبة: ${activeAirMass['temp']} • ${activeAirMass['humidity']}', + style: const TextStyle(color: Colors.white70, fontSize: 11.5), + ), + const SizedBox(height: 4), + Text( + 'المصدر الجغرافي: ${activeAirMass['source']}', + style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 11), + ), + const SizedBox(height: 4), + Text( + 'أثرها المباشر في الأردن: ${activeAirMass['jordanImpact']}', + style: const TextStyle(color: AppColors.guardianAmber, fontSize: 11, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ], + ); + } +} + +/// Dynamic Custom Painter illustrating atmospheric cross-section: +/// ground, warm air wedge, cold air undercut, dynamic cloud rendering, and rain streams. +class _AtmosphericFrontPainter extends CustomPainter { + final int frontMode; // 0=Cold, 1=Warm, 2=Stationary, 3=Occluded + final String airMassCode; + final Color airMassColor; + final double progress; + + _AtmosphericFrontPainter({ + required this.frontMode, + required this.airMassCode, + required this.airMassColor, + required this.progress, + }); + + @override + void paint(Canvas canvas, Size size) { + final w = size.width; + final h = size.height; + + // 1. Sky & Atmospheric background gradient + final skyPaint = Paint() + ..shader = const LinearGradient( + colors: [Color(0xFF0B1B36), Color(0xFF071020)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ).createShader(Rect.fromLTWH(0, 0, w, h)); + canvas.drawRect(Rect.fromLTWH(0, 0, w, h), skyPaint); + + // 2. Ground surface + final groundY = h * 0.85; + final groundPaint = Paint()..color = const Color(0xFF1E293B); + canvas.drawRect(Rect.fromLTWH(0, groundY, w, h - groundY), groundPaint); + + // Ground grass line + final grassPaint = Paint() + ..color = const Color(0xFF10B981) + ..strokeWidth = 2.5; + canvas.drawLine(Offset(0, groundY), Offset(w, groundY), grassPaint); + + // 3. Draw Front Boundary and Air wedges based on mode + if (frontMode == 0) { + // COLD FRONT: Steep cold wedge pushing rightward under warm air + _drawColdFront(canvas, w, h, groundY); + } else if (frontMode == 1) { + // WARM FRONT: Gentle slope, warm air gliding up over retreating cold air + _drawWarmFront(canvas, w, h, groundY); + } else if (frontMode == 2) { + // STATIONARY FRONT: Two opposing air masses side-by-side + _drawStationaryFront(canvas, w, h, groundY); + } else { + // OCCLUDED FRONT: Cold air catches up, lifting warm pocket completely aloft + _drawOccludedFront(canvas, w, h, groundY); + } + + // 4. Draw Air Mass Telemetry Watermark on canvas + final tagPainter = TextPainter( + text: TextSpan( + text: 'كتلة هوائية نشطة: $airMassCode', + style: TextStyle( + color: airMassColor.withAlpha(200), + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + textDirection: TextDirection.rtl, + )..layout(); + tagPainter.paint(canvas, Offset(w - tagPainter.width - 16, 16)); + } + + void _drawColdFront(Canvas canvas, double w, double h, double groundY) { + // Cold air wedge (steep slope on left) + final coldWedgePath = Path() + ..moveTo(0, groundY) + ..lineTo(w * 0.55, groundY) + ..quadraticBezierTo(w * 0.48, groundY - 140, 0, groundY - 190) + ..close(); + + final coldPaint = Paint() + ..color = const Color(0xFF2563EB).withAlpha(120) + ..style = PaintingStyle.fill; + canvas.drawPath(coldWedgePath, coldPaint); + + // Cold air label + _drawText(canvas, 'هواء بارد كثيف (Cold Air)', Offset(w * 0.12, groundY - 50), + Colors.white70, 11); + + // Warm air pushed upwards + final warmArrowPaint = Paint() + ..color = const Color(0xFFEF4444).withAlpha(190) + ..strokeWidth = 2.5 + ..style = PaintingStyle.stroke; + canvas.drawLine( + Offset(w * 0.65, groundY - 40), Offset(w * 0.50, groundY - 150), warmArrowPaint); + _drawText(canvas, 'هواء دافئ يرتفع بقوة ⇈', Offset(w * 0.54, groundY - 170), + const Color(0xFFFCA5A5), 11); + + // Towering Cumulonimbus clouds at boundary + _drawCloud(canvas, Offset(w * 0.46, groundY - 180), 55, const Color(0xFF475569)); + _drawCloud(canvas, Offset(w * 0.52, groundY - 150), 45, const Color(0xFF334155)); + _drawCloud(canvas, Offset(w * 0.48, groundY - 110), 40, const Color(0xFF1E293B)); + + // Heavy rain streams under cloud + final rainPaint = Paint() + ..color = const Color(0xFF38BDF8).withAlpha(180) + ..strokeWidth = 1.8; + for (int i = 0; i < 8; i++) { + final rx = w * 0.42 + (i * 14); + final ry = groundY - 90 + ((progress * 40 + i * 10) % 80); + canvas.drawLine(Offset(rx, ry), Offset(rx - 4, ry + 16), rainPaint); + } + + // Front boundary line with blue triangles + final frontLinePaint = Paint() + ..color = const Color(0xFF38BDF8) + ..strokeWidth = 3; + canvas.drawLine( + Offset(w * 0.48, groundY - 140), Offset(w * 0.55, groundY), frontLinePaint); + + // Draw blue triangle markers + _drawFrontMarkerTriangle( + canvas, Offset(w * 0.50, groundY - 80), const Color(0xFF2563EB)); + } + + void _drawWarmFront(Canvas canvas, double w, double h, double groundY) { + // Cold retreating air wedge (gentle slope on right) + final coldWedgePath = Path() + ..moveTo(w, groundY) + ..lineTo(w * 0.20, groundY) + ..lineTo(w, groundY - 180) + ..close(); + + final coldPaint = Paint() + ..color = const Color(0xFF3B82F6).withAlpha(90) + ..style = PaintingStyle.fill; + canvas.drawPath(coldWedgePath, coldPaint); + + _drawText(canvas, 'هواء بارد ينسحب ببطء', Offset(w * 0.65, groundY - 40), + Colors.white70, 11); + + // Warm air gliding over cold wedge + final warmArrowPaint = Paint() + ..color = const Color(0xFFEF4444).withAlpha(190) + ..strokeWidth = 2.5 + ..style = PaintingStyle.stroke; + canvas.drawLine( + Offset(w * 0.10, groundY - 20), Offset(w * 0.70, groundY - 160), warmArrowPaint); + _drawText(canvas, 'هواء دافئ يصعد بانحدار لطيف ↗', Offset(w * 0.15, groundY - 110), + const Color(0xFFFCA5A5), 11); + + // Layered Stratus Clouds spread wide + _drawCloud(canvas, Offset(w * 0.45, groundY - 120), 45, const Color(0xFF64748B)); + _drawCloud(canvas, Offset(w * 0.65, groundY - 150), 40, const Color(0xFF94A3B8)); + _drawCloud(canvas, Offset(w * 0.85, groundY - 175), 30, const Color(0xFFCBD5E1)); + + // Gentle continuous rain + final rainPaint = Paint() + ..color = const Color(0xFF67E8F9).withAlpha(130) + ..strokeWidth = 1.2; + for (int i = 0; i < 10; i++) { + final rx = w * 0.35 + (i * 20); + final ry = groundY - 70 + ((progress * 30 + i * 8) % 65); + canvas.drawLine(Offset(rx, ry), Offset(rx - 2, ry + 12), rainPaint); + } + + // Front boundary line with red semicircles + final frontLinePaint = Paint() + ..color = const Color(0xFFEF4444) + ..strokeWidth = 3; + canvas.drawLine( + Offset(w * 0.20, groundY), Offset(w * 0.80, groundY - 140), frontLinePaint); + + // Draw red semicircle marker + _drawFrontMarkerSemicircle( + canvas, Offset(w * 0.45, groundY - 60), const Color(0xFFDC2626)); + } + + void _drawStationaryFront(Canvas canvas, double w, double h, double groundY) { + // Air masses abutting in the middle + canvas.drawRect( + Rect.fromLTWH(0, groundY - 160, w * 0.5, 160), + Paint()..color = const Color(0xFF2563EB).withAlpha(70), + ); + canvas.drawRect( + Rect.fromLTWH(w * 0.5, groundY - 160, w * 0.5, 160), + Paint()..color = const Color(0xFFDC2626).withAlpha(70), + ); + + _drawText(canvas, 'كتلة باردة ←', Offset(w * 0.15, groundY - 60), Colors.white, 12); + _drawText(canvas, '→ كتلة دافئة', Offset(w * 0.65, groundY - 60), Colors.white, 12); + _drawText(canvas, 'توازن القوى (لا تقدم لأي طرف)', Offset(w * 0.32, groundY - 20), + AppColors.guardianAmber, 11); + + // Stationary front line + final linePaint = Paint() + ..color = const Color(0xFF8B5CF6) + ..strokeWidth = 3; + canvas.drawLine(Offset(w * 0.5, groundY), Offset(w * 0.5, groundY - 160), linePaint); + + // Draw clouds at boundary + _drawCloud(canvas, Offset(w * 0.5, groundY - 140), 45, const Color(0xFF475569)); + } + + void _drawOccludedFront(Canvas canvas, double w, double h, double groundY) { + // Cold air undercuts from both sides, warm pocket lifted aloft + final coldWedge1 = Path() + ..moveTo(0, groundY) + ..lineTo(w * 0.65, groundY) + ..lineTo(0, groundY - 160) + ..close(); + canvas.drawPath(coldWedge1, Paint()..color = const Color(0xFF1D4ED8).withAlpha(120)); + + // Warm air lifted pocket + final warmPocket = Path() + ..moveTo(w * 0.35, groundY - 110) + ..quadraticBezierTo(w * 0.50, groundY - 190, w * 0.65, groundY - 110) + ..close(); + canvas.drawPath(warmPocket, Paint()..color = const Color(0xFFEF4444).withAlpha(180)); + + _drawText(canvas, 'هواء دافئ معزول بالكامل في الأعلى', + Offset(w * 0.28, groundY - 180), const Color(0xFFFECACA), 11); + _drawText(canvas, 'هواء بارد سطحي', Offset(w * 0.15, groundY - 30), Colors.white70, 11); + + // Occluded purple boundary + final linePaint = Paint() + ..color = const Color(0xFF7C3AED) + ..strokeWidth = 3; + canvas.drawLine( + Offset(w * 0.50, groundY), Offset(w * 0.50, groundY - 110), linePaint); + } + + void _drawCloud(Canvas canvas, Offset center, double radius, Color color) { + final cloudPaint = Paint()..color = color; + canvas.drawCircle(center, radius, cloudPaint); + canvas.drawCircle(Offset(center.dx - radius * 0.6, center.dy + radius * 0.2), + radius * 0.75, cloudPaint); + canvas.drawCircle(Offset(center.dx + radius * 0.6, center.dy + radius * 0.2), + radius * 0.75, cloudPaint); + } + + void _drawFrontMarkerTriangle(Canvas canvas, Offset center, Color color) { + final path = Path() + ..moveTo(center.dx, center.dy - 10) + ..lineTo(center.dx + 12, center.dy + 4) + ..lineTo(center.dx - 2, center.dy + 10) + ..close(); + canvas.drawPath(path, Paint()..color = color); + } + + void _drawFrontMarkerSemicircle(Canvas canvas, Offset center, Color color) { + final rect = Rect.fromCircle(center: center, radius: 8); + canvas.drawArc(rect, 0, math.pi, true, Paint()..color = color); + } + + void _drawText( + Canvas canvas, String text, Offset offset, Color color, double fontSize) { + final tp = TextPainter( + text: TextSpan( + text: text, + style: TextStyle( + color: color, + fontSize: fontSize, + fontWeight: FontWeight.w700, + ), + ), + textDirection: TextDirection.rtl, + )..layout(); + tp.paint(canvas, offset); + } + + @override + bool shouldRepaint(covariant _AtmosphericFrontPainter old) { + return old.frontMode != frontMode || + old.airMassCode != airMassCode || + old.airMassColor != airMassColor || + old.progress != progress; + } +} diff --git a/apps/student_app/lib/presentation/screens/virtual_labs/lab_identity.dart b/apps/student_app/lib/presentation/screens/virtual_labs/lab_identity.dart index eccb37a..8c32d93 100644 --- a/apps/student_app/lib/presentation/screens/virtual_labs/lab_identity.dart +++ b/apps/student_app/lib/presentation/screens/virtual_labs/lab_identity.dart @@ -95,6 +95,17 @@ class LabIdentity { // LESSON-BOUND LAB IDENTITIES (verified against spec front-matter) // --------------------------------------------------------------------------- +const LabIdentity kPhysicsVectorsIntroLabIdentity = LabIdentity( + curriculumLessonId: 'physics_10_semester_1_unit_01_lesson_01', + subjectKey: 'physics_10', + semesterKey: 'semester_1', + unitKey: 'unit_01', + lessonKey: 'lesson_01', + sourceMarkdown: 'grade_10/physics_10/semester_1/unit_01/lesson_01.md', + subjectAr: 'الفيزياء', + lessonAr: 'الكميات القياسية والمتجهة وتمثيلها', +); + const LabIdentity kPhysicsVectorAdditionLabIdentity = LabIdentity( curriculumLessonId: 'physics_10_semester_1_unit_01_lesson_02', subjectKey: 'physics_10', @@ -117,6 +128,17 @@ const LabIdentity kPhysicsMotion1DLabIdentity = LabIdentity( lessonAr: 'الحركة في بعد واحد', ); +const LabIdentity kPhysicsProjectileMotionLabIdentity = LabIdentity( + curriculumLessonId: 'physics_10_semester_1_unit_02_lesson_02', + subjectKey: 'physics_10', + semesterKey: 'semester_1', + unitKey: 'unit_02', + lessonKey: 'lesson_02', + sourceMarkdown: 'grade_10/physics_10/semester_1/unit_02/lesson_02.md', + subjectAr: 'الفيزياء', + lessonAr: 'حركة المقذوفات في بعدين', +); + const LabIdentity kPhysicsCircularMotionLabIdentity = LabIdentity( curriculumLessonId: 'physics_10_semester_2_unit_04_lesson_03', subjectKey: 'physics_10', @@ -196,6 +218,17 @@ const LabIdentity kEarthRockCycleLabIdentity = LabIdentity( lessonAr: 'دورة الصخور', ); +const LabIdentity kEarthAirMassesLabIdentity = LabIdentity( + curriculumLessonId: 'earth_sciences_10_semester_2_unit_03_lesson_01', + subjectKey: 'earth_sciences_10', + semesterKey: 'semester_2', + unitKey: 'unit_03', + lessonKey: 'lesson_01', + sourceMarkdown: 'grade_10/earth_sciences_10/semester_2/unit_03/lesson_01.md', + subjectAr: 'علوم الأرض والبيئة', + lessonAr: 'الكتل والجبهات الهوائية', +); + const LabIdentity kMathSystemsLabIdentity = LabIdentity( curriculumLessonId: 'math_10_semester_1_unit_01_lesson_02', subjectKey: 'math_10', @@ -440,6 +473,20 @@ const LabIdentity kArabicUnit2VocativeLabIdentity = LabIdentity( lessonAr: 'أبني لغتي (1) — أسلوبُ النّداءِ', ); +/// Unit 2, lesson 6 — build my language (2): the requestive construction (الإنشاءُ الطلبيُّ). +/// Built on pages 56-59: concept of insha talabi (cannot be verified/falsified), +/// six types (النداء، الأمر، النهي، الاستفهام، التمني، الترجي) with tools and examples. +const LabIdentity kArabicUnit2InshaLabIdentity = LabIdentity( + curriculumLessonId: 'arabic_10_semester_1_unit_02_lesson_06', + subjectKey: 'arabic_10', + semesterKey: 'semester_1', + unitKey: 'unit_02', + lessonKey: 'lesson_06', + sourceMarkdown: 'grade_10/arabic_10/semester_1/unit_02/lesson_06.md', + subjectAr: 'العربية لغتي', + lessonAr: 'أبني لغتي (2) — الأسلوبُ الإنشائيّ (الإنشاءُ الطّلبيُّ)', +); + // --------------------------------------------------------------------------- // STANDALONE AUTHORING TOOLS (NO curriculum-lesson anchor in the corpus) // --------------------------------------------------------------------------- @@ -504,18 +551,33 @@ const LabIdentity kArabicProsodyToolIdentity = LabIdentity( lessonAr: 'العروض والموسيقى الشعرية', ); -const LabIdentity kIslamicTajweedToolIdentity = LabIdentity( - toolKey: 'islamic_tajweed', +const LabIdentity kIslamicTajweedLabIdentity = LabIdentity( + gradeKey: 'grade_10', + subjectKey: 'islamic_10', + semesterKey: 'semester_1', + unitKey: 'unit_01', + lessonKey: 'lesson_01', + curriculumLessonId: 'islamic_10_semester_1_unit_01_lesson_01', + lessonAr: 'واجب المسلم تجاه القرآن الكريم (أحكام التلاوة والمخارج)', subjectAr: 'التربية الإسلامية', - lessonAr: 'التجويد ومخارج الحروف', + sourceMarkdown: 'grade_10/islamic_10/semester_1/unit_01/lesson_01.md', ); -const LabIdentity kIslamicInheritanceToolIdentity = LabIdentity( - toolKey: 'islamic_inheritance', +const LabIdentity kIslamicInheritanceLabIdentity = LabIdentity( + gradeKey: 'grade_10', + subjectKey: 'islamic_10', + semesterKey: 'semester_1', + unitKey: 'unit_01', + lessonKey: 'lesson_02', + curriculumLessonId: 'islamic_10_semester_1_unit_01_lesson_02', + lessonAr: 'فقه المعاملات والفرائض (حاسبة المواريث والأنصبة)', subjectAr: 'التربية الإسلامية', - lessonAr: 'المواريث والفرائض', + sourceMarkdown: 'grade_10/islamic_10/semester_1/unit_01/lesson_02.md', ); +const LabIdentity kIslamicTajweedToolIdentity = kIslamicTajweedLabIdentity; +const LabIdentity kIslamicInheritanceToolIdentity = kIslamicInheritanceLabIdentity; + const LabIdentity kFinanceBudgetToolIdentity = LabIdentity( toolKey: 'finance_budget', subjectAr: 'الثقافة المالية', diff --git a/apps/student_app/lib/presentation/screens/virtual_labs/labs_registry.dart b/apps/student_app/lib/presentation/screens/virtual_labs/labs_registry.dart index e3b7ec1..45f8003 100644 --- a/apps/student_app/lib/presentation/screens/virtual_labs/labs_registry.dart +++ b/apps/student_app/lib/presentation/screens/virtual_labs/labs_registry.dart @@ -4,10 +4,13 @@ import '../../../data/models/socratic_checkpoint_model.dart'; import '../../widgets/socratic_dialog.dart'; import 'lab_identity.dart'; import 'lab_scaffold.dart'; +import 'physics_vectors_intro_lab.dart'; +import 'physics_projectile_motion_lab.dart'; import 'physics_labs.dart'; import 'chemistry_labs.dart'; import 'biology_labs.dart'; import 'earth_labs.dart'; +import 'earth_air_masses_lab.dart'; import 'math_labs.dart'; import 'english_labs.dart'; import 'islamic_labs.dart'; @@ -28,6 +31,7 @@ import 'arabic_unit2_vocative_lab.dart'; import 'arabic_unit2_writing_lab.dart'; import 'arabic_unit2_poetry_lab.dart'; import 'arabic_unit2_speaking_lab.dart'; +import 'arabic_unit2_insha_lab.dart'; /// ============================================================================ /// GRADE-10 VIRTUAL LABS REGISTRY @@ -65,7 +69,11 @@ class Grade10LabsRegistry { // Bound lesson labs (17) and standalone authoring tools (16). // --------------------------------------------------------------------------- static final List all = [ - // ---- Physics (4: 1 bound + 3 bound) ---- + // ---- Physics (6: 6 bound) ---- + Grade10LabEntry( + identity: kPhysicsVectorsIntroLabIdentity, + builder: (cb) => PhysicsVectorsIntroLabView(onCheckpointTriggered: cb), + ), Grade10LabEntry( identity: kPhysicsVectorAdditionLabIdentity, builder: (cb) => PhysicsVectorAdditionLabView(onCheckpointTriggered: cb), @@ -74,6 +82,11 @@ class Grade10LabsRegistry { identity: kPhysicsMotion1DLabIdentity, builder: (cb) => PhysicsMotion1DLabView(onCheckpointTriggered: cb), ), + Grade10LabEntry( + identity: kPhysicsProjectileMotionLabIdentity, + builder: (cb) => + PhysicsProjectileMotionLabView(onCheckpointTriggered: cb), + ), Grade10LabEntry( identity: kPhysicsCircularMotionLabIdentity, builder: (cb) => PhysicsCircularMotionLabView(onCheckpointTriggered: cb), @@ -117,7 +130,7 @@ class Grade10LabsRegistry { identity: kBiologyMicroscopeToolIdentity, builder: (cb) => BiologyMicroscopeLabView(onCheckpointTriggered: cb), ), - // ---- Earth (3: 2 tools + 1 bound) ---- + // ---- Earth (4: 2 tools + 2 bound) ---- Grade10LabEntry( identity: kEarthMohsHardnessToolIdentity, builder: (cb) => EarthMohsHardnessLabView(onCheckpointTriggered: cb), @@ -130,6 +143,10 @@ class Grade10LabsRegistry { identity: kEarthStratigraphyToolIdentity, builder: (cb) => EarthStratigraphyLabView(onCheckpointTriggered: cb), ), + Grade10LabEntry( + identity: kEarthAirMassesLabIdentity, + builder: (cb) => EarthAirMassesLabView(onCheckpointTriggered: cb), + ), // ---- Math (3 bound) ---- Grade10LabEntry( identity: kMathSystemsLabIdentity, @@ -152,7 +169,11 @@ class Grade10LabsRegistry { identity: kEnglishTenseToolIdentity, builder: (cb) => EnglishTenseTimelineLabView(onCheckpointTriggered: cb), ), -// ---- Arabic (13: 2 tools + 11 bound) ---- +// ---- Arabic (14: 2 tools + 12 bound) ---- + Grade10LabEntry( + identity: kArabicUnit2InshaLabIdentity, + builder: (cb) => ArabicUnit2InshaLabView(onCheckpointTriggered: cb), + ), Grade10LabEntry( identity: kArabicUnit2VocativeLabIdentity, builder: (cb) => ArabicUnit2VocativeLabView(onCheckpointTriggered: cb), @@ -207,13 +228,13 @@ class Grade10LabsRegistry { identity: kArabicProsodyToolIdentity, builder: (cb) => ArabicProsodyLabView(onCheckpointTriggered: cb), ), - // ---- Islamic (2 tools) ---- + // ---- Islamic (2 bound curriculum labs) ---- Grade10LabEntry( - identity: kIslamicTajweedToolIdentity, + identity: kIslamicTajweedLabIdentity, builder: (cb) => IslamicTajweedLabView(onCheckpointTriggered: cb), ), Grade10LabEntry( - identity: kIslamicInheritanceToolIdentity, + identity: kIslamicInheritanceLabIdentity, builder: (cb) => IslamicInheritanceLabView(onCheckpointTriggered: cb), ), // ---- Finance (3: 1 tool + 2 bound) ---- @@ -382,7 +403,7 @@ class Grade10LabsRegistry { } } } - return candidates.first; + return null; } } diff --git a/apps/student_app/lib/presentation/screens/virtual_labs/physics_projectile_motion_lab.dart b/apps/student_app/lib/presentation/screens/virtual_labs/physics_projectile_motion_lab.dart new file mode 100644 index 0000000..6e1e442 --- /dev/null +++ b/apps/student_app/lib/presentation/screens/virtual_labs/physics_projectile_motion_lab.dart @@ -0,0 +1,645 @@ +import 'dart:math' as math; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import '../../../core/theme/app_colors.dart'; +import 'lab_identity.dart'; +import 'lab_scaffold.dart'; + +/// ============================================================================ +/// PHYSICS — GRADE 10 VIRTUAL LAB (Jordanian MoE curriculum) +/// Unit 2, Lesson 2: حركة المقذوفات في بعدين (Projectile Motion) +/// Curriculum Lesson ID: physics_10_semester_1_unit_02_lesson_02 +/// +/// Textbook mapping (Pages 57-66): +/// - Horizontal motion: constant speed ax = 0, Vx = V0 cos(θ) +/// - Vertical motion: free fall ay = -g, Vy = V0 sin(θ) - g t +/// - Time to apex: th = (V0 sin θ) / g +/// - Total flight time: T = 2 th = (2 V0 sin θ) / g +/// - Maximum height: h = (V0 sin θ)^2 / (2g) +/// - Range: R = (V0^2 sin(2θ)) / g (Max range at 45°; equal for complementary angles θ & 90-θ) +/// ============================================================================ + +class PhysicsProjectileMotionLabView extends StatefulWidget { + final LabCheckpointCallback? onCheckpointTriggered; + const PhysicsProjectileMotionLabView({super.key, this.onCheckpointTriggered}); + + @override + State createState() => + _PhysicsProjectileMotionLabViewState(); +} + +class _PhysicsProjectileMotionLabViewState + extends State + with SingleTickerProviderStateMixin { + double _v0 = 24.0; // Initial velocity m/s (10..40) + double _angleDeg = 45.0; // Launch angle (15..80) + final double _gravity = 9.8; // m/s^2 + + bool _showVelocityVectors = true; + bool _showComplementaryTrajectory = false; + bool _slowMotion = false; + + // Animation / simulation state: + bool _isPlaying = false; + double _simTime = 0.0; // elapsed time in seconds + late final AnimationController _animCtl; + final List _firedPoints = []; + + // Target challenge: + final double _targetDistance = 48.0; // meters + + @override + void initState() { + super.initState(); + _animCtl = AnimationController( + vsync: this, + duration: const Duration(seconds: 10), + )..addListener(_tickSimulation); + } + + void _tickSimulation() { + if (!_isPlaying) return; + final totalFlightTime = _calcFlightTime(_v0, _angleDeg); + final dt = _slowMotion ? 0.012 : 0.028; + + setState(() { + _simTime += dt; + final currentPos = _calcPositionAtTime(_simTime, _v0, _angleDeg); + _firedPoints.add(currentPos); + + if (_simTime >= totalFlightTime) { + _simTime = totalFlightTime; + _isPlaying = false; + _animCtl.stop(); + saqelTick(); + } + }); + } + + @override + void dispose() { + _animCtl.dispose(); + super.dispose(); + } + + // --- Physics Helpers --- + double _calcFlightTime(double v0, double ang) { + final rad = ang * math.pi / 180.0; + return (2.0 * v0 * math.sin(rad)) / _gravity; + } + + double _calcMaxHeight(double v0, double ang) { + final rad = ang * math.pi / 180.0; + final vy0 = v0 * math.sin(rad); + return (vy0 * vy0) / (2.0 * _gravity); + } + + double _calcRange(double v0, double ang) { + final rad = ang * math.pi / 180.0; + return (v0 * v0 * math.sin(2.0 * rad)) / _gravity; + } + + Offset _calcPositionAtTime(double t, double v0, double ang) { + final rad = ang * math.pi / 180.0; + final vx = v0 * math.cos(rad); + final vy0 = v0 * math.sin(rad); + final x = vx * t; + final y = vy0 * t - 0.5 * _gravity * t * t; + return Offset(x, math.max(0.0, y)); + } + + void _launchProjectile() { + saqelTick(); + setState(() { + _isPlaying = true; + _simTime = 0.0; + _firedPoints.clear(); + }); + _animCtl.repeat(); + } + + void _resetSimulation() { + saqelTick(); + setState(() { + _isPlaying = false; + _simTime = 0.0; + _firedPoints.clear(); + _animCtl.reset(); + }); + } + + @override + Widget build(BuildContext context) { + final flightTime = _calcFlightTime(_v0, _angleDeg); + final maxHeight = _calcMaxHeight(_v0, _angleDeg); + final totalRange = _calcRange(_v0, _angleDeg); + + // Current instant values: + final rad = _angleDeg * math.pi / 180.0; + final curVx = _v0 * math.cos(rad); + final curVy = _v0 * math.sin(rad) - _gravity * _simTime; + final isAtApex = curVy.abs() < 1.0; + final isTargetHit = (_calcRange(_v0, _angleDeg) - _targetDistance).abs() <= 2.2; + + return SaqelLabScaffold( + titleAr: 'حركة المقذوفات في بُعدين', + subtitleAr: 'مسار منحني • مركبتان متعامدتان vx و vy • المدى الأقصى والارتفاع', + identity: kPhysicsProjectileMotionLabIdentity, + onCheckpointTriggered: widget.onCheckpointTriggered, + checkpointQuestion: + 'عند وصول المقذوف إلى أقصى ارتفاع رأسي (Apex)، كم تكون قيمة المركبة الرأسية للسرعة vy؟', + checkpointOptions: const [ + 'تساوي صفراً لحظياً، بينما تبقى السرعة الأفقية vx ثابتة', + 'تكون في قيمتها العظمى القصوى متجهة لأعلى', + 'تساوي تسارع الجاذبية الأرضية g مضروباً في 2', + 'تنعكس فوراً دون المرور بنقطة السكون اللحظي', + ], + checkpointCorrectIdx: 0, + telemetry: [ + LabPill('المدى R = ${totalRange.toStringAsFixed(1)} m'), + LabPill('أقصى ارتفاع h = ${maxHeight.toStringAsFixed(1)} m', + color: const Color(0xFFFF9F0A)), + LabPill('زمن التحليق T = ${flightTime.toStringAsFixed(2)} s', + color: const Color(0xFF30D158)), + if (isTargetHit) + const LabPill('إصابة مباشرة للهدف! 🎯', color: Color(0xFF00F5D4)), + ], + canvas: CustomPaint( + painter: _ProjectileCanvasPainter( + v0: _v0, + angleDeg: _angleDeg, + gravity: _gravity, + simTime: _simTime, + isPlaying: _isPlaying, + showVelocityVectors: _showVelocityVectors, + showComplementary: _showComplementaryTrajectory, + targetDistance: _targetDistance, + firedPoints: List.of(_firedPoints), + ), + child: Container(), + ), + controls: [ + // Launch and Reset row: + Row( + children: [ + Expanded( + flex: 2, + child: ElevatedButton.icon( + icon: Icon( + _isPlaying ? CupertinoIcons.pause_fill : CupertinoIcons.play_arrow_solid, + size: 18, + ), + label: Text( + _isPlaying ? 'إيقاف مؤقت' : 'إطلاق القذيفة 🚀', + style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 13), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.saqelCyan, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + onPressed: _launchProjectile, + ), + ), + const SizedBox(width: 8), + Expanded( + flex: 1, + child: OutlinedButton.icon( + icon: const Icon(CupertinoIcons.arrow_counterclockwise, size: 16), + label: const Text('إعادة', style: TextStyle(fontSize: 12)), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.white70, + side: const BorderSide(color: Colors.white24), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + onPressed: _resetSimulation, + ), + ), + ], + ), + const SizedBox(height: 12), + + // Angle Slider with presets: + LabSlider( + label: 'زاوية الإطلاق θ', + value: _angleDeg, + min: 15.0, + max: 80.0, + display: '${_angleDeg.toInt()}°', + accent: _angleDeg == 45.0 ? const Color(0xFF00F5D4) : const Color(0xFFFF9F0A), + onChanged: (v) => setState(() { + _angleDeg = v; + if (!_isPlaying) _firedPoints.clear(); + }), + ), + + // Quick angle presets: + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildPresetChip('30°', 30.0), + const SizedBox(width: 6), + _buildPresetChip('45° (أقصى مدى)', 45.0, isSpecial: true), + const SizedBox(width: 6), + _buildPresetChip('60° (متممة لـ 30°)', 60.0), + const SizedBox(width: 6), + _buildPresetChip('53° (مثال 12 ص61)', 53.0), + ], + ), + ), + const SizedBox(height: 10), + + // Initial Velocity Slider: + LabSlider( + label: 'السرعة الابتدائية v₀', + value: _v0, + min: 12.0, + max: 36.0, + display: '${_v0.toStringAsFixed(1)} m/s', + onChanged: (v) => setState(() { + _v0 = v; + if (!_isPlaying) _firedPoints.clear(); + }), + ), + + const SizedBox(height: 8), + LabToggle( + label: 'إظهار مركبات السرعة المتجهة (vx و vy)', + hint: 'vx أفقية ثابتة • vy رأسية تتغير بالجاذبية', + value: _showVelocityVectors, + onChanged: (v) => setState(() => _showVelocityVectors = v), + ), + LabToggle( + label: 'مقارنة الزاوية المتممة (${(90 - _angleDeg).toInt()}°)', + hint: 'الزاويتان المتتامتان لهما المدى الأفقي نفسه R', + value: _showComplementaryTrajectory, + onChanged: (v) => setState(() => _showComplementaryTrajectory = v), + ), + LabToggle( + label: 'تصوير بالحركة البطيئة (Slow Motion)', + hint: 'لدراسة انعدام vy عند القمة (Apex)', + value: _slowMotion, + onChanged: (v) => setState(() => _slowMotion = v), + ), + + const SizedBox(height: 10), + LabFormulaCard( + title: isAtApex && _isPlaying + ? 'القمة اللحظية! vy = 0 m/s و vx = ${curVx.toStringAsFixed(1)} m/s' + : 'معادلات حركة المقذوفات (منهاج الوزارة)', + body: + 'vx = v₀·cosθ = ${curVx.toStringAsFixed(1)} m/s (ثابتة دوماً)\n' + 'vy = v₀·sinθ − g·t = ${curVy.toStringAsFixed(1)} m/s\n' + 'المدى الأفقي: R = (v₀²·sin 2θ) / g = ${totalRange.toStringAsFixed(1)} m\n' + 'أقصى ارتفاع: h = (v₀·sinθ)² / 2g = ${maxHeight.toStringAsFixed(1)} m', + ), + ], + footerNote: + 'بإهمال مقاومة الهواء • نموذج المقذوفات الأردني المعتمد (الوحدة 2: الصفحات 57 - 66).', + ); + } + + Widget _buildPresetChip(String label, double val, {bool isSpecial = false}) { + final isSelected = (_angleDeg - val).abs() < 0.5; + return GestureDetector( + onTap: () => setState(() { + saqelTick(); + _angleDeg = val; + if (!_isPlaying) _firedPoints.clear(); + }), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: isSelected + ? (isSpecial ? AppColors.saqelCyan : const Color(0xFFFF9F0A)) + : Colors.white.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isSelected ? Colors.transparent : Colors.white24, + width: 1, + ), + ), + child: Text( + label, + style: TextStyle( + color: isSelected ? Colors.black : Colors.white70, + fontSize: 11, + fontWeight: isSelected ? FontWeight.w800 : FontWeight.w600, + ), + ), + ), + ); + } +} + +/// ============================================================================ +/// CANVAS PAINTER: Projectile Flight, Vectors, and Target +/// ============================================================================ +class _ProjectileCanvasPainter extends CustomPainter { + final double v0; + final double angleDeg; + final double gravity; + final double simTime; + final bool isPlaying; + final bool showVelocityVectors; + final bool showComplementary; + final double targetDistance; + final List firedPoints; + + _ProjectileCanvasPainter({ + required this.v0, + required this.angleDeg, + required this.gravity, + required this.simTime, + required this.isPlaying, + required this.showVelocityVectors, + required this.showComplementary, + required this.targetDistance, + required this.firedPoints, + }); + + @override + void paint(Canvas c, Size s) { + // 1. Dark space backdrop + c.drawRect( + Offset.zero & s, + Paint() + ..shader = const LinearGradient( + colors: [Color(0xFF07111F), Color(0xFF0B1728)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ).createShader(Offset.zero & s), + ); + + final groundY = s.height * 0.82; + const originX = 42.0; + + // Scale factors from meters to screen pixels: + // Max horizontal range is around 130m, we scale to fit width: + final scaleX = (s.width - 80.0) / 100.0; + final scaleY = (groundY - 30.0) / 45.0; + + // 2. Draw Distance Grid and Ground + _drawGroundAndGrid(c, s, groundY, originX, scaleX); + + // 3. Draw Target Marker + final targetScreenX = originX + targetDistance * scaleX; + _drawTarget(c, targetScreenX, groundY); + + // 4. Draw Theoretical Trajectory (Parabola) + _drawParabola(c, originX, groundY, scaleX, scaleY, v0, angleDeg, + const Color(0xFF00F5D4).withValues(alpha: 0.55), isDashed: true); + + // 5. If complementary angle enabled (90 - theta): + if (showComplementary) { + final compAngle = 90.0 - angleDeg; + _drawParabola(c, originX, groundY, scaleX, scaleY, v0, compAngle, + const Color(0xFFFF9F0A).withValues(alpha: 0.45), isDashed: true); + } + + // 6. Draw Cannon / Launcher at origin + _drawCannon(c, originX, groundY, angleDeg); + + // 7. Draw Trajectory Trail of Fired Points + if (firedPoints.isNotEmpty) { + final trailPath = Path(); + for (int i = 0; i < firedPoints.length; i++) { + final sx = originX + firedPoints[i].dx * scaleX; + final sy = groundY - firedPoints[i].dy * scaleY; + if (i == 0) { + trailPath.moveTo(sx, sy); + } else { + trailPath.lineTo(sx, sy); + } + } + c.drawPath( + trailPath, + Paint() + ..color = AppColors.saqelCyan.withValues(alpha: 0.85) + ..style = PaintingStyle.stroke + ..strokeWidth = 2.4, + ); + } + + // 8. Draw Moving Projectile Ball and Velocity Vectors + final rad = angleDeg * math.pi / 180.0; + final curX = v0 * math.cos(rad) * simTime; + final curY = v0 * math.sin(rad) * simTime - 0.5 * gravity * simTime * simTime; + + final ballX = originX + curX * scaleX; + final ballY = groundY - math.max(0.0, curY) * scaleY; + + // Glowing projectile ball + c.drawCircle( + Offset(ballX, ballY), + 8.0, + Paint() + ..color = const Color(0xFF00F5D4) + ..maskFilter = const MaskFilter.blur(BlurStyle.solid, 4), + ); + c.drawCircle(Offset(ballX, ballY), 5.5, Paint()..color = Colors.white); + + // Velocity Vectors on the ball: + if (showVelocityVectors && curY >= 0.0) { + final vx = v0 * math.cos(rad); + final vy = v0 * math.sin(rad) - gravity * simTime; + + // Horizontal Vx vector (Cyan, constant) + _drawVec(c, Offset(ballX, ballY), Offset(ballX + vx * 1.5, ballY), + const Color(0xFF00F5D4), 'vx'); + + // Vertical Vy vector (Amber, dynamic) + if (vy.abs() > 0.8) { + _drawVec(c, Offset(ballX, ballY), Offset(ballX, ballY - vy * 1.5), + const Color(0xFFFF9F0A), 'vy'); + } + + // Gravitational acceleration vector g (pointing down) + _drawVec(c, Offset(ballX, ballY), Offset(ballX, ballY + 28), + const Color(0xFFFF375F), 'g'); + } + } + + void _drawGroundAndGrid( + Canvas c, Size s, double groundY, double originX, double scaleX) { + // Ground line + final groundPaint = Paint() + ..color = const Color(0xFF1E293B) + ..strokeWidth = 4; + c.drawLine(Offset(0, groundY), Offset(s.width, groundY), groundPaint); + + // Grass / surface accents + final grass = Paint() + ..color = const Color(0xFF10B981).withValues(alpha: 0.6) + ..strokeWidth = 2; + c.drawLine(Offset(0, groundY), Offset(s.width, groundY), grass); + + // Ticks every 10 meters + for (int m = 10; m <= 90; m += 10) { + final tx = originX + m * scaleX; + if (tx > s.width - 10) break; + c.drawLine( + Offset(tx, groundY), + Offset(tx, groundY + 6), + Paint()..color = Colors.white30, + ); + final tp = TextPainter( + text: TextSpan( + text: '$m m', + style: const TextStyle(color: Colors.white38, fontSize: 9), + ), + textDirection: TextDirection.ltr, + )..layout(); + tp.paint(c, Offset(tx - tp.width / 2, groundY + 8)); + } + } + + void _drawTarget(Canvas c, double tx, double groundY) { + // Target base flag / bullseye + c.drawCircle( + Offset(tx, groundY), + 9, + Paint() + ..color = const Color(0xFFFF375F).withValues(alpha: 0.3) + ..style = PaintingStyle.fill, + ); + c.drawCircle( + Offset(tx, groundY), + 9, + Paint() + ..color = const Color(0xFFFF375F) + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + ); + c.drawCircle(Offset(tx, groundY), 3.5, Paint()..color = Colors.white); + + // Flag pole + c.drawLine( + Offset(tx, groundY), + Offset(tx, groundY - 24), + Paint() + ..color = Colors.white70 + ..strokeWidth = 1.5, + ); + final flagPath = Path() + ..moveTo(tx, groundY - 24) + ..lineTo(tx + 14, groundY - 18) + ..lineTo(tx, groundY - 12) + ..close(); + c.drawPath(flagPath, Paint()..color = const Color(0xFFFF375F)); + } + + void _drawParabola( + Canvas c, + double originX, + double groundY, + double scaleX, + double scaleY, + double v0, + double ang, + Color col, { + bool isDashed = false, + }) { + final rad = ang * math.pi / 180.0; + final totalFlightTime = (2.0 * v0 * math.sin(rad)) / gravity; + final path = Path(); + + const steps = 60; + for (int i = 0; i <= steps; i++) { + final t = (totalFlightTime * i) / steps; + final x = v0 * math.cos(rad) * t; + final y = v0 * math.sin(rad) * t - 0.5 * gravity * t * t; + + final sx = originX + x * scaleX; + final sy = groundY - math.max(0.0, y) * scaleY; + + if (i == 0) { + path.moveTo(sx, sy); + } else { + path.lineTo(sx, sy); + } + } + + c.drawPath( + path, + Paint() + ..color = col + ..style = PaintingStyle.stroke + ..strokeWidth = 1.6, + ); + } + + void _drawCannon(Canvas c, double ox, double gy, double angDeg) { + c.save(); + c.translate(ox, gy); + + // Cannon base wheel + c.drawCircle(const Offset(0, -6), 11, Paint()..color = const Color(0xFF334155)); + c.drawCircle( + const Offset(0, -6), + 11, + Paint() + ..color = Colors.white30 + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5, + ); + + // Barrel rotation + c.rotate(-angDeg * math.pi / 180.0); + final barrelRect = + RRect.fromRectAndRadius(const Rect.fromLTWH(0, -5, 26, 10), const Radius.circular(3)); + c.drawRRect(barrelRect, Paint()..color = AppColors.saqelCyan); + c.drawRRect( + barrelRect, + Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 1.2, + ); + + c.restore(); + } + + void _drawVec(Canvas c, Offset a, Offset b, Color col, String label) { + final p = Paint() + ..color = col + ..strokeWidth = 2.4 + ..strokeCap = StrokeCap.round; + c.drawLine(a, b, p); + + final ang = math.atan2(b.dy - a.dy, b.dx - a.dx); + const hs = 7.0; + final p1 = b - Offset(math.cos(ang - 0.45) * hs, math.sin(ang - 0.45) * hs); + final p2 = b - Offset(math.cos(ang + 0.45) * hs, math.sin(ang + 0.45) * hs); + c.drawPath( + Path() + ..moveTo(b.dx, b.dy) + ..lineTo(p1.dx, p1.dy) + ..lineTo(p2.dx, p2.dy) + ..close(), + Paint()..color = col, + ); + + final tp = TextPainter( + text: TextSpan( + text: label, + style: TextStyle(color: col, fontSize: 10, fontWeight: FontWeight.w900), + ), + textDirection: TextDirection.ltr, + )..layout(); + tp.paint(c, b + const Offset(3, -12)); + } + + @override + bool shouldRepaint(covariant _ProjectileCanvasPainter o) => + o.v0 != v0 || + o.angleDeg != angleDeg || + o.simTime != simTime || + o.isPlaying != isPlaying || + o.showVelocityVectors != showVelocityVectors || + o.showComplementary != showComplementary || + o.firedPoints.length != firedPoints.length; +} diff --git a/apps/student_app/lib/presentation/screens/virtual_labs/physics_vectors_intro_lab.dart b/apps/student_app/lib/presentation/screens/virtual_labs/physics_vectors_intro_lab.dart new file mode 100644 index 0000000..cf3ada2 --- /dev/null +++ b/apps/student_app/lib/presentation/screens/virtual_labs/physics_vectors_intro_lab.dart @@ -0,0 +1,739 @@ +import 'dart:math' as math; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import '../../../core/theme/app_colors.dart'; +import 'lab_identity.dart'; +import 'lab_scaffold.dart'; + +/// ============================================================================ +/// PHYSICS — GRADE 10 VIRTUAL LAB (Jordanian MoE curriculum) +/// Unit 1, Lesson 1: الكميات القياسية والكميات المتجهة وتمثيلها بيانياً +/// Curriculum Lesson ID: physics_10_semester_1_unit_01_lesson_01 +/// +/// Direct textbook mapping (Pages 5-19): +/// 1. ظاهرة هبوط الطائرات في الرياح المتقاطعة (Crosswind Landing - صفحة 5) +/// - توجيه الطائرة ضد الرياح لتكون السرعة المحصلة منطبقة على محور المدرج. +/// 2. تمثيل المتجهات وخصائصها ومضاعفاتها وسالب المتجه (صفحات 8-12) +/// - سحب المتجه، تغيير مقياس الرسم، ضرب المتجه بكمية قياسية n، وسالب المتجه (-A). +/// 3. ضرب المتجهات: الضرب القياسي (A·B = AB cos θ) والضرب المتجهي (|A×B| = AB sin θ) +/// - عرض مساحة متوازي الأضلاع وقاعدة اليد اليمنى وحالة التساوي عند θ = 45°. +/// ============================================================================ + +class PhysicsVectorsIntroLabView extends StatefulWidget { + final LabCheckpointCallback? onCheckpointTriggered; + const PhysicsVectorsIntroLabView({super.key, this.onCheckpointTriggered}); + + @override + State createState() => + _PhysicsVectorsIntroLabViewState(); +} + +class _PhysicsVectorsIntroLabViewState extends State + with SingleTickerProviderStateMixin { + // 0: هبوط الرياح المتقاطعة, 1: تمثيل وسالب المتجه, 2: الضرب النقطي والتقاطعي + int _activeTab = 0; + + // --- TAB 0: Crosswind Landing --- + double _planeHeading = -18.0; // Heading deviation in degrees (-45..+45) + double _windSpeed = 22.0; // Crosswind speed knots (-40..+40) + final double _planeAirspeed = 70.0; // knots airspeed + bool _isLanding = false; + double _landingProgress = 0.0; + late final AnimationController _landingCtl; + + // --- TAB 1: Vector Representation & Properties --- + double _vecMag = 80.0; // magnitude (20..140) + double _vecAngle = 40.0; // angle in degrees (0..360) + double _scalarMultiplier = 1.5; // n in [-2.0..2.0] + int _selectedQuantityIdx = 0; // 0 = Force (vector), 1 = Mass (scalar) + + // --- TAB 2: Dot & Cross Product --- + double _dotMagA = 90.0; + double _dotMagB = 75.0; + double _dotAngle = 45.0; // Angle between A and B (0..180) + + @override + void initState() { + super.initState(); + _landingCtl = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2200), + )..addListener(() { + setState(() { + _landingProgress = _landingCtl.value; + if (_landingCtl.isCompleted) { + _isLanding = false; + } + }); + }); + } + + @override + void dispose() { + _landingCtl.dispose(); + super.dispose(); + } + + void _triggerLanding() { + saqelTick(); + setState(() { + _isLanding = true; + _landingProgress = 0.0; + }); + _landingCtl.forward(from: 0.0); + } + + @override + Widget build(BuildContext context) { + // Crosswind physics calculations: + // Runway is along the Y axis (0 deg is straight down the runway). + // Plane velocity: Vx = V_air * sin(heading), Vy = V_air * cos(heading) + // Wind velocity: Wx = windSpeed (pure crosswind), Wy = 0 + final headingRad = _planeHeading * math.pi / 180.0; + final vxPlane = _planeAirspeed * math.sin(headingRad); + final vyPlane = _planeAirspeed * math.cos(headingRad); + final vxGround = vxPlane + _windSpeed; + final vyGround = vyPlane; + final vGroundMag = math.sqrt(vxGround * vxGround + vyGround * vyGround); + final groundTrackDeg = math.atan2(vxGround, vyGround) * 180.0 / math.pi; + final bool isAligned = groundTrackDeg.abs() <= 3.0; + + // Dot and Cross physics calculations: + final radTheta = _dotAngle * math.pi / 180.0; + final dotProduct = _dotMagA * _dotMagB * math.cos(radTheta) / 100.0; + final crossProduct = _dotMagA * _dotMagB * math.sin(radTheta) / 100.0; + final bool isEqualed45 = (_dotAngle - 45.0).abs() <= 1.5; + + return SaqelLabScaffold( + titleAr: 'الكميات القياسية والمتجهة وتمثيلها', + subtitleAr: 'الرياح المتقاطعة • تمثيل وسالب المتجهات • الضرب القياسي والمتجهي', + identity: kPhysicsVectorsIntroLabIdentity, + onCheckpointTriggered: widget.onCheckpointTriggered, + checkpointQuestion: + 'في تجربة هبوط الطائرات مع رياح متقاطعة (Crosswind)، لتفادي خروج الطائرة عن المدرج يجب أن تكون …', + checkpointOptions: const [ + 'السرعة المحصلة لسرعتي الطائرة والرياح منطبقة على محور المدرج', + 'مقدمة الطائرة موازية تماماً للمدرج بغض النظر عن الرياح', + 'سرعة الرياح مساوية لسرعة الطائرة في المقدار ومعاكسة لها', + 'السرعة المحصلة متعامدة على المدرج لتثبيت العجلات', + ], + checkpointCorrectIdx: 0, + telemetry: _buildTelemetry(isAligned, vGroundMag, groundTrackDeg, dotProduct, crossProduct), + canvas: _buildCanvas(isAligned, groundTrackDeg, dotProduct, crossProduct), + controls: _buildControls(isAligned, isEqualed45, groundTrackDeg), + footerNote: + 'مبني وموثق طبقاً للمنهاج الأردني المعتمد (الوحدة 1: الصفحات 5 - 19) • لا يعتمد بيانات وهمية.', + ); + } + + List _buildTelemetry( + bool isAligned, + double vGroundMag, + double groundTrackDeg, + double dotProduct, + double crossProduct, + ) { + if (_activeTab == 0) { + return [ + LabPill('السرعة الأرضية |Vg| = ${vGroundMag.toStringAsFixed(1)} knot'), + LabPill( + 'الانحراف = ${groundTrackDeg.toStringAsFixed(1)}°', + color: isAligned ? const Color(0xFF30D158) : const Color(0xFFFF453A), + ), + LabPill( + isAligned ? 'مسار منطبق ومثالي ✅' : 'مسار هبوط منحرف ⚠️', + color: isAligned ? const Color(0xFF30D158) : const Color(0xFFFF9F0A), + ), + ]; + } else if (_activeTab == 1) { + final resMag = (_vecMag * _scalarMultiplier.abs()).toStringAsFixed(1); + return [ + LabPill('المقدار الأساسي |A| = ${_vecMag.toInt()} N'), + LabPill('الزاوية θ = ${_vecAngle.toInt()}°', color: const Color(0xFFFF9F0A)), + LabPill('المتجه الناتج |n·A| = $resMag N', color: const Color(0xFF00F5D4)), + if (_scalarMultiplier < 0) + const LabPill('سالب المتجه (−180°)', color: Color(0xFFFF375F)), + ]; + } else { + return [ + LabPill('الضرب القياسي A·B = ${dotProduct.toStringAsFixed(1)} J'), + LabPill( + 'الضرب المتجهي |A×B| = ${crossProduct.toStringAsFixed(1)} N·m', + color: const Color(0xFFFF9F0A), + ), + LabPill( + _dotAngle <= 90 ? 'اتجاه المتجه: خارج الصفحة ⊙' : 'اتجاه المتجه: داخل الصفحة ⊗', + color: const Color(0xFF30D158), + ), + ]; + } + } + + Widget _buildCanvas( + bool isAligned, + double groundTrackDeg, + double dotProduct, + double crossProduct, + ) { + return GestureDetector( + onPanUpdate: _activeTab == 1 ? _handleVectorDrag : null, + child: CustomPaint( + painter: _VectorsIntroCanvasPainter( + activeTab: _activeTab, + planeHeading: _planeHeading, + windSpeed: _windSpeed, + planeAirspeed: _planeAirspeed, + isLanding: _isLanding, + landingProgress: _landingProgress, + vecMag: _vecMag, + vecAngle: _vecAngle, + scalarMultiplier: _scalarMultiplier, + dotMagA: _dotMagA, + dotMagB: _dotMagB, + dotAngle: _dotAngle, + ), + child: Container(), + ), + ); + } + + void _handleVectorDrag(DragUpdateDetails d) { + final box = context.findRenderObject() as RenderBox?; + if (box == null) return; + final local = box.globalToLocal(d.globalPosition); + final cx = box.size.width * 0.45; + final cy = box.size.height * 0.52; + final dx = local.dx - cx; + final dy = -(local.dy - cy); + var ang = math.atan2(dy, dx) * 180.0 / math.pi; + if (ang < 0) ang += 360.0; + final dist = math.sqrt(dx * dx + dy * dy); + setState(() { + _vecAngle = ang.clamp(0.0, 360.0); + _vecMag = dist.clamp(25.0, 130.0); + }); + } + + List _buildControls(bool isAligned, bool isEqualed45, double groundTrackDeg) { + return [ + LabSegments( + labels: const ['هبوط الرياح المتقاطعة', 'تمثيل وسالب المتجه', 'الضرب النقطي والتقاطعي'], + values: const [0, 1, 2], + current: _activeTab, + onSelected: (tab) => setState(() { + saqelTick(); + _activeTab = tab; + }), + ), + const SizedBox(height: 12), + + if (_activeTab == 0) ...[ + // TAB 0 CONTROLS + LabSlider( + label: 'توجيه مقدمة الطائرة (Heading)', + value: _planeHeading, + min: -40.0, + max: 40.0, + display: '${_planeHeading.toStringAsFixed(1)}°', + accent: const Color(0xFF00F5D4), + onChanged: (v) => setState(() => _planeHeading = v), + ), + LabSlider( + label: 'سرعة الرياح الجانبية (Crosswind)', + value: _windSpeed, + min: -35.0, + max: 35.0, + display: '${_windSpeed.toStringAsFixed(1)} knot', + accent: const Color(0xFFFF9F0A), + onChanged: (v) => setState(() => _windSpeed = v), + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + icon: Icon( + _isLanding ? CupertinoIcons.airplane : CupertinoIcons.arrow_down_circle_fill, + size: 18, + ), + label: Text( + _isLanding ? 'جاري الهبوط التجريبي...' : 'تنفيذ هبوط تجريبي على المدرج 🛬', + style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 13), + ), + style: ElevatedButton.styleFrom( + backgroundColor: isAligned ? AppColors.saqelCyan : const Color(0xFFFF9F0A), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + padding: const EdgeInsets.symmetric(vertical: 11), + ), + onPressed: _isLanding ? null : _triggerLanding, + ), + ), + const SizedBox(height: 10), + LabFormulaCard( + title: 'الفيزياء المنهجية (صفحة 5 من الكتاب)', + body: + 'السرعة المحصلة = سرعة الطائرة بالنسبة للهواء + سرعة الرياح\n' + 'V_ground = V_plane + V_wind\n' + 'الانحراف الحالي: ${groundTrackDeg.toStringAsFixed(1)}°\n' + '${isAligned ? "✅ زاوية التوجيه عادلت سرعة الرياح بدقة واستقام المسار." : "⚠️ اضبط توجيه مقدمة الطائرة لعكس اتجاه دفع الرياح."}', + ), + ] else if (_activeTab == 1) ...[ + // TAB 1 CONTROLS + LabSegments( + labels: const ['قوة F (كمية متجهة)', 'كتلة m (كمية قياسية)'], + values: const [0, 1], + current: _selectedQuantityIdx, + onSelected: (i) => setState(() => _selectedQuantityIdx = i), + ), + const SizedBox(height: 8), + LabSlider( + label: 'المقدار |A|', + value: _vecMag, + min: 25.0, + max: 130.0, + display: '${_vecMag.toInt()} N', + onChanged: (v) => setState(() => _vecMag = v), + ), + LabSlider( + label: 'الاتجاه θA من محور السينات الموجب', + value: _vecAngle, + min: 0.0, + max: 360.0, + display: '${_vecAngle.toInt()}°', + accent: const Color(0xFFFF9F0A), + onChanged: (v) => setState(() => _vecAngle = v), + ), + LabSlider( + label: 'معامل الضرب القياسي n (مضاعفة / سالب المتجه)', + value: _scalarMultiplier, + min: -2.0, + max: 2.0, + display: 'n = ${_scalarMultiplier.toStringAsFixed(2)}', + accent: _scalarMultiplier < 0 ? const Color(0xFFFF375F) : const Color(0xFF30D158), + onChanged: (v) => setState(() => _scalarMultiplier = v), + ), + const SizedBox(height: 8), + const LabFormulaCard( + title: 'خصائص المتجهات المنهجية (صفحة 11)', + body: + 'سالب المتجه (-A): نفس المقدار ويعاكسه تماماً في الاتجاه (180°).\n' + 'ضرب المتجه في عدد قياسي n: يصبح المقدار |n|·A، ويبقى بالاتجاه نفسه إذا n>0 وينعكس إذا n<0.', + ), + ] else ...[ + // TAB 2 CONTROLS + LabSlider( + label: 'الزاوية المحصورة بين المتجهين θ', + value: _dotAngle, + min: 0.0, + max: 180.0, + display: '${_dotAngle.toInt()}°', + accent: isEqualed45 ? const Color(0xFF00F5D4) : const Color(0xFFFF9F0A), + onChanged: (v) => setState(() => _dotAngle = v), + ), + LabSlider( + label: 'مقدار المتجه A', + value: _dotMagA, + min: 30.0, + max: 120.0, + display: '${_dotMagA.toInt()} N', + onChanged: (v) => setState(() => _dotMagA = v), + ), + LabSlider( + label: 'مقدار المتجه B', + value: _dotMagB, + min: 30.0, + max: 120.0, + display: '${_dotMagB.toInt()} m', + accent: const Color(0xFF60A5FA), + onChanged: (v) => setState(() => _dotMagB = v), + ), + const SizedBox(height: 8), + LabFormulaCard( + title: isEqualed45 + ? 'ملاحظة ذهبية: يتساوى الضرب القياسي والمتجهي عند θ = 45°!' + : 'قوانين الضرب (صفحات 13 - 15 من الكتاب)', + body: + 'الضرب القياسي (الشغل W): A·B = A·B·cosθ\n' + 'الضرب المتجهي (العزم τ): |A×B| = A·B·sinθ\n' + 'عند θ = 90°: الضرب النقطي ينعدم، والمتجهي يكون في قيمته العظمى.\n' + 'عند θ = 45°: tan(45°) = 1 ⟹ A·B = |A×B|.', + ), + ], + ]; + } +} + +/// ============================================================================ +/// CANVAS PAINTER: 3 Physics Visualizations +/// ============================================================================ +class _VectorsIntroCanvasPainter extends CustomPainter { + final int activeTab; + final double planeHeading; + final double windSpeed; + final double planeAirspeed; + final bool isLanding; + final double landingProgress; + final double vecMag; + final double vecAngle; + final double scalarMultiplier; + final double dotMagA; + final double dotMagB; + final double dotAngle; + + _VectorsIntroCanvasPainter({ + required this.activeTab, + required this.planeHeading, + required this.windSpeed, + required this.planeAirspeed, + required this.isLanding, + required this.landingProgress, + required this.vecMag, + required this.vecAngle, + required this.scalarMultiplier, + required this.dotMagA, + required this.dotMagB, + required this.dotAngle, + }); + + @override + void paint(Canvas c, Size s) { + // Dark void background + c.drawRect( + Offset.zero & s, + Paint() + ..shader = const LinearGradient( + colors: [Color(0xFF07111F), Color(0xFF0B1728)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ).createShader(Offset.zero & s), + ); + + if (activeTab == 0) { + _paintCrosswindRunway(c, s); + } else if (activeTab == 1) { + _paintVectorProperties(c, s); + } else { + _paintDotAndCrossProduct(c, s); + } + } + + // --------------------------------------------------------------------------- + // TAB 0: Crosswind Runway + // --------------------------------------------------------------------------- + void _paintCrosswindRunway(Canvas c, Size s) { + final cx = s.width * 0.48; + const rWidth = 84.0; + + // Runway surface + final runwayRect = Rect.fromLTWH(cx - rWidth / 2, 12, rWidth, s.height - 24); + c.drawRRect( + RRect.fromRectAndRadius(runwayRect, const Radius.circular(8)), + Paint()..color = const Color(0xFF131D2D), + ); + c.drawRRect( + RRect.fromRectAndRadius(runwayRect, const Radius.circular(8)), + Paint() + ..color = Colors.white.withValues(alpha: 0.18) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.6, + ); + + // Centerline dashed markings + final dashPaint = Paint() + ..color = Colors.white70 + ..strokeWidth = 2.4; + for (double y = runwayRect.top + 20; y < runwayRect.bottom - 20; y += 28) { + c.drawLine(Offset(cx, y), Offset(cx, y + 14), dashPaint); + } + + // Runway threshold "08" + final tpRunway = TextPainter( + text: const TextSpan( + text: '08', + style: TextStyle( + color: Colors.white60, + fontSize: 16, + fontWeight: FontWeight.w900, + letterSpacing: 2, + ), + ), + textDirection: TextDirection.ltr, + )..layout(); + tpRunway.paint(c, Offset(cx - tpRunway.width / 2, runwayRect.bottom - 36)); + + // Calculate aircraft position: + final headingRad = planeHeading * math.pi / 180.0; + final vxPlane = planeAirspeed * math.sin(headingRad); + final vyPlane = planeAirspeed * math.cos(headingRad); + final vxGround = vxPlane + windSpeed; + + // Normalised position on canvas: + final startY = runwayRect.top + 38; + final endY = runwayRect.bottom - 50; + final curY = isLanding ? startY + (endY - startY) * landingProgress : startY + 50.0; + // Ground drift + final driftFactor = (vxGround / planeAirspeed) * 90.0; + final curX = isLanding ? cx + driftFactor * landingProgress : cx; + final planePos = Offset(curX, curY); + + // Draw Vector diagram originating from the plane: + final vAirEnd = planePos + Offset(vxPlane * 1.1, vyPlane * 0.9); + final vWindEnd = vAirEnd + Offset(windSpeed * 1.6, 0); + + // 1. Plane Airspeed Vector (Cyan) + _drawArrow(c, planePos, vAirEnd, AppColors.saqelCyan, 'سرعة الطائرة V_air'); + + // 2. Crosswind Vector (Amber) + _drawArrow(c, vAirEnd, vWindEnd, const Color(0xFFFF9F0A), 'رياح جانبية V_wind'); + + // 3. Ground Resultant Vector (Green or Red) + final isAligned = (vxGround / planeAirspeed).abs() < 0.08; + final resColor = isAligned ? const Color(0xFF30D158) : const Color(0xFFFF453A); + _drawArrow(c, planePos, vWindEnd, resColor, 'السرعة المحصلة V_ground', width: 3.4); + + // Draw Airplane at planePos rotated by planeHeading + c.save(); + c.translate(planePos.dx, planePos.dy); + c.rotate(headingRad); + _drawAirplaneIcon(c, isAligned); + c.restore(); + } + + void _drawAirplaneIcon(Canvas c, bool isAligned) { + final bodyPaint = Paint()..color = Colors.white; + final wingPaint = Paint()..color = isAligned ? AppColors.saqelCyan : const Color(0xFFFF9F0A); + + // Fuselage + c.drawRRect( + RRect.fromRectAndRadius( + const Rect.fromLTWH(-4, -18, 8, 36), + const Radius.circular(4), + ), + bodyPaint, + ); + // Wings + final wingPath = Path() + ..moveTo(0, -3) + ..lineTo(-22, 10) + ..lineTo(-22, 6) + ..lineTo(0, -9) + ..lineTo(22, 6) + ..lineTo(22, 10) + ..close(); + c.drawPath(wingPath, wingPaint); + // Tail + final tailPath = Path() + ..moveTo(0, 10) + ..lineTo(-9, 17) + ..lineTo(9, 17) + ..close(); + c.drawPath(tailPath, wingPaint); + } + + // --------------------------------------------------------------------------- + // TAB 1: Vector Properties & Multiplication + // --------------------------------------------------------------------------- + void _paintVectorProperties(Canvas c, Size s) { + final cx = s.width * 0.44; + final cy = s.height * 0.52; + final origin = Offset(cx, cy); + + _drawGridAndAxes(c, s, cx, cy); + + final aRad = vecAngle * math.pi / 180.0; + final ax = vecMag * math.cos(aRad); + final ay = -vecMag * math.sin(aRad); + final aEnd = origin + Offset(ax, ay); + + // Draw original Vector A + _drawArrow(c, origin, aEnd, AppColors.saqelCyan, 'A (${vecMag.toInt()} N)', width: 3.0); + + // Draw Resultant Vector B = n * A + final bx = ax * scalarMultiplier; + final by = ay * scalarMultiplier; + final bEnd = origin + Offset(bx, by); + + final bColor = scalarMultiplier < 0 ? const Color(0xFFFF375F) : const Color(0xFF30D158); + final bLabel = scalarMultiplier < 0 + ? 'سالب المتجه (−${scalarMultiplier.abs().toStringAsFixed(1)} A)' + : 'n·A (${scalarMultiplier.toStringAsFixed(1)} A)'; + + if ((scalarMultiplier - 1.0).abs() > 0.05) { + _drawArrow(c, origin, bEnd, bColor, bLabel, width: 3.2, offsetText: 22); + } + + // Origin dot + c.drawCircle(origin, 5, Paint()..color = Colors.white); + } + + // --------------------------------------------------------------------------- + // TAB 2: Dot & Cross Product (Parallelogram & Angle Arc) + // --------------------------------------------------------------------------- + void _paintDotAndCrossProduct(Canvas c, Size s) { + final cx = s.width * 0.38; + final cy = s.height * 0.58; + final origin = Offset(cx, cy); + + _drawGridAndAxes(c, s, cx, cy); + + // Vector A along positive X axis: + final aEnd = origin + Offset(dotMagA, 0); + + // Vector B at angle theta: + final radTheta = dotAngle * math.pi / 180.0; + final bx = dotMagB * math.cos(radTheta); + final by = -dotMagB * math.sin(radTheta); + final bEnd = origin + Offset(bx, by); + + // 1. Shaded Parallelogram for Cross Product (Area = |A × B|) + final pFar = bEnd + Offset(dotMagA, 0); + final paraPath = Path() + ..moveTo(origin.dx, origin.dy) + ..lineTo(aEnd.dx, aEnd.dy) + ..lineTo(pFar.dx, pFar.dy) + ..lineTo(bEnd.dx, bEnd.dy) + ..close(); + + c.drawPath( + paraPath, + Paint() + ..color = const Color(0xFFFF9F0A).withValues(alpha: 0.18) + ..style = PaintingStyle.fill, + ); + c.drawPath( + paraPath, + Paint() + ..color = const Color(0xFFFF9F0A).withValues(alpha: 0.4) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.2, + ); + + // 2. Shaded Projection for Dot Product (Shadow of B on A) + final projX = origin.dx + bx; + c.drawLine( + bEnd, + Offset(projX, origin.dy), + Paint() + ..color = Colors.white38 + ..strokeWidth = 1.2 + ..strokeCap = StrokeCap.round, + ); + c.drawRect( + Rect.fromLTRB( + math.min(origin.dx, projX), + origin.dy - 3, + math.max(origin.dx, projX), + origin.dy + 3, + ), + Paint()..color = const Color(0xFF30D158), + ); + + // 3. Draw Vector Arrows + _drawArrow(c, origin, aEnd, AppColors.saqelCyan, 'A', width: 3.2); + _drawArrow(c, origin, bEnd, const Color(0xFF60A5FA), 'B', width: 3.2); + + // 4. Angle Arc + c.drawArc( + Rect.fromCircle(center: origin, radius: 36), + 0, + -radTheta, + false, + Paint() + ..color = const Color(0xFFFFD60A) + ..strokeWidth = 2.0 + ..style = PaintingStyle.stroke, + ); + final tpAngle = TextPainter( + text: TextSpan( + text: 'θ = ${dotAngle.toInt()}°', + style: const TextStyle( + color: Color(0xFFFFD60A), + fontSize: 12, + fontWeight: FontWeight.w800, + ), + ), + textDirection: TextDirection.ltr, + )..layout(); + tpAngle.paint(c, origin + const Offset(42, -26)); + + // Origin dot + c.drawCircle(origin, 5, Paint()..color = Colors.white); + } + + // --------------------------------------------------------------------------- + // HELPER PAINTERS + // --------------------------------------------------------------------------- + void _drawGridAndAxes(Canvas c, Size s, double cx, double cy) { + final grid = Paint() + ..color = Colors.white.withValues(alpha: 0.05) + ..strokeWidth = 1; + for (double x = 0; x < s.width; x += 26) { + c.drawLine(Offset(x, 0), Offset(x, s.height), grid); + } + for (double y = 0; y < s.height; y += 26) { + c.drawLine(Offset(0, y), Offset(s.width, y), grid); + } + + final axis = Paint() + ..color = Colors.white.withValues(alpha: 0.22) + ..strokeWidth = 1.5; + c.drawLine(Offset(0, cy), Offset(s.width, cy), axis); + c.drawLine(Offset(cx, 0), Offset(cx, s.height), axis); + } + + void _drawArrow( + Canvas c, + Offset a, + Offset b, + Color col, + String label, { + double width = 2.6, + double offsetText = 14, + }) { + final p = Paint() + ..color = col + ..strokeWidth = width + ..strokeCap = StrokeCap.round; + c.drawLine(a, b, p); + + final ang = math.atan2(b.dy - a.dy, b.dx - a.dx); + const hs = 10.0; + final p1 = b - Offset(math.cos(ang - 0.45) * hs, math.sin(ang - 0.45) * hs); + final p2 = b - Offset(math.cos(ang + 0.45) * hs, math.sin(ang + 0.45) * hs); + c.drawPath( + Path() + ..moveTo(b.dx, b.dy) + ..lineTo(p1.dx, p1.dy) + ..lineTo(p2.dx, p2.dy) + ..close(), + Paint()..color = col, + ); + + // Draggable handle glow + c.drawCircle(b, 7, Paint()..color = col.withValues(alpha: 0.28)); + c.drawCircle(b, 3.5, Paint()..color = col); + + final tp = TextPainter( + text: TextSpan( + text: label, + style: TextStyle(color: col, fontSize: 11, fontWeight: FontWeight.w900), + ), + textDirection: TextDirection.rtl, + )..layout(); + tp.paint(c, b + Offset(4, -offsetText)); + } + + @override + bool shouldRepaint(covariant _VectorsIntroCanvasPainter o) => + o.activeTab != activeTab || + o.planeHeading != planeHeading || + o.windSpeed != windSpeed || + o.isLanding != isLanding || + o.landingProgress != landingProgress || + o.vecMag != vecMag || + o.vecAngle != vecAngle || + o.scalarMultiplier != scalarMultiplier || + o.dotMagA != dotMagA || + o.dotMagB != dotMagB || + o.dotAngle != dotAngle; +} diff --git a/apps/student_app/lib/presentation/screens/vocational/vocational_training_screen.dart b/apps/student_app/lib/presentation/screens/vocational/vocational_training_screen.dart index 50f4abc..3b0d568 100644 --- a/apps/student_app/lib/presentation/screens/vocational/vocational_training_screen.dart +++ b/apps/student_app/lib/presentation/screens/vocational/vocational_training_screen.dart @@ -11,7 +11,7 @@ import '../../widgets/luxury_widgets.dart'; /// بوابة منظومة مؤسسة التدريب المهني (VTC) والتعليم التقني والتدريب المهني: /// 1. دليل الـ 140 مهنة وحرفة موزعة على 8 قطاعات إنتاجية وصناعية وطنية. /// 2. تخصص صيانة وتشخيص المركبات الكهربائية والهجينة (EV & Hybrid Specialist). -/// 3. محاكي فحص بطاريات الجهد العالي ونظام العزل (HVIL & BMS 120 FPS Lab). +/// 3. محاكي هيكل السيارة البصري، تنظيم القطاعات، وأدوات القياس والعداد الرقمي. /// 4. نظام الكفايات المهنية (CBT) ومعايير السلامة المهنية للمركبات الكهربائية (NFPA 70E). class VocationalTrainingScreen extends StatefulWidget { const VocationalTrainingScreen({super.key}); @@ -25,21 +25,238 @@ class _VocationalTrainingScreenState extends State int _selectedSectorIndex = 0; int _activeTabIndex = 0; // 0 = 140 Trades Directory, 1 = EV Diagnostic Lab, 2 = CBT Modules - // EV Lab Parameters - double _batterySoc = 82.0; // State of Charge % + // EV Car Lab Parameters & Interactive Components + int _selectedComponentIndex = 0; + int _componentDetailTab = 0; // 0 = تشريح القطاع والرسم, 1 = طريقة الربط, 2 = الفحص وقراءة العداد + bool _probesPlaced = true; + bool _servicePlugRemoved = false; + final double _batterySoc = 82.0; // State of Charge % double _packVoltage = 384.0; // Volts DC double _isolationResistance = 5.2; // Mega-Ohms (Threshold > 0.5 MΩ) bool _hvilInterlockClosed = true; // High Voltage Interlock Loop - bool _contactorPrechargeActive = true; String _activeDtcCode = 'P0000: No Faults Detected (Normal Operation)'; - bool _simulationRunning = true; late final AnimationController _pulseController; + final List> _carComponents = [ + { + 'id': 'battery', + 'title': 'بطارية الجهد العالي (High-Voltage Traction Battery)', + 'subtitle': '384V DC • كيمياء الليثيوم NMC • مدمجة بأرضية الشاسيه', + 'location': 'في أرضية السيارة السفلية بين المحورين الأمامي والخلفي لخفض مركز الثقل وزيادة الثبات وحماية الركاب.', + 'function': 'تخزين وتفريغ الطاقة الكهربائية الرئيسية لتغذية محرك السيارة والأنظمة عالية القدرة (تكييف كهربائي وتدفئة PTC).', + 'connection': 'تتصل بالمحول الأمامي عبر كابلات برتقالية سميكة (+ و -)، وتتصل بمنفذ الشحن الخلفي، ويقطع دائرتها قابس الأمان في المنتصف.', + 'howToInspect': 'ضع المجس الأسود للملتيميتر على القطب السالب (-) والمجس الأحمر على القطب الموجب (+) في نقطة الفحص المخصصة.', + 'meterReading': '384.0', + 'meterUnit': 'V DC', + 'meterMode': 'جهد مستمر DCV', + 'probeRed': 'القطب الموجب (+HV)', + 'probeBlack': 'القطب السالب (-HV)', + 'normalRange': '350V - 400V DC (طبيعي)', + 'safetyStatus': 'جهد قاتل! ارتداء قفازات الفئة Class 0 (1000V) إلزامي قبل الاقتراب.', + 'color': const Color(0xFF10B981), + 'icon': CupertinoIcons.battery_full, + 'internalParts': [ + 'وحدات الخلايا (12 Modules): خلايا ليثيوم أيون NMC موصولة على التوالي والتوازي بجهد إجمالي 384V DC.', + 'نظام إدارة البطارية (BMS Controller): مراقبة فولتية كل خلية، درجات الحرارة، وموازنة الشحن النشط.', + 'قواطع التوصيل الرئيسية (SMR+/SMR-): ريليهات كهرومغناطيسية محكمة العزل لفصل وتوصيل القطبين.', + 'دائرة الشحن المسبق (Pre-charge Circuit): مقاومة تحد من تدفق التيار المفاجئ لحماية مكثفات المحول.', + 'مقبس قاطع الخدمة (MSD Socket): نقطة قطع ميكانيكي تقسم الحزمة لنصفين معزولين بجهد أقل من 200V.', + ], + 'connectionsDetail': [ + 'خط الإخراج الرئيسي: كابلات برتقالية سميكة تنقل 384V إلى محول القدرة (Inverter) في المقدمة.', + 'خط الشحن السريع: كابلات متجهة إلى مقبس الشحن CCS2 في مؤخرة المركبة.', + 'خط التغذية المنخفضة: كابل إلى محول خفض الجهد DC-DC لتغذية بطارية 12V المساعدة.', + 'حلقة أمان الجهد العالي (HVIL): سلك إشارة تسلسلي يمر بجميع المكونات، أي قطع يفتح قواطع SMR فوراً.', + ], + 'inspectionProtocol': [ + 'الخطوة 1: ارتدِ قفازات عازلة معتمدة Class 0 (1000V) ونظارات واقية، وتأكد من جفاف اليدين تماماً.', + 'الخطوة 2: اضبط الملتيميتر الرقمي على وضع DCV (جهد مستمر حتى 600V أو 1000V).', + 'الخطوة 3: ضع المجس الأحمر (+) على الطرف الموجب والمجس الأسود (-) على الطرف السالب في نقطة الفحص.', + 'الخطوة 4: القراءة السليمة: 350V - 400V في وضع التشغيل. بعد سحب قابس MSD يجب أن تهبط إلى 0.00V.', + ], + }, + { + 'id': 'inverter_motor', + 'title': 'محول القدرة والمحرك الكهربائي (Inverter & Traction Motor)', + 'subtitle': 'تحويل 384V DC إلى تيار متناوب 3-Phase AC لتوليد العزم الحركي', + 'location': 'في حوض المحرك الأمامي متصل مباشرة بمحور العجلات الأمامية وعلبة التروس التخفيضية.', + 'function': 'تحويل التيار المستمر من البطارية إلى تيار متردد لتشغيل المحرك، ويعمل كمولد لشحن البطارية عند الكبح التجديدي (Regenerative Braking).', + 'connection': 'يستقبل خطوط الجهد العالي (+HV و -HV) من البطارية، ويخرج 3 خطوط متناوبة (U, V, W) إلى ملفات المحرك الكهربائي.', + 'howToInspect': 'قياس الجهد المستمر الداخل (يجب أن يكون 0.00V بعد نزع القابس)، وقياس مقاومة أطوار المحرك (U-V-W) للتأكد من توازنها.', + 'meterReading': '0.18', + 'meterUnit': 'Ω', + 'meterMode': 'مقاومة ملفات المحرك Ω', + 'probeRed': 'الطور الأول (Phase U)', + 'probeBlack': 'الطور الثاني (Phase V)', + 'normalRange': '0.15 Ω - 0.25 Ω (ملفات متزنة وسليمة)', + 'safetyStatus': 'تفريغ مكثفات المحول يستغرق 5 دقائق بعد فصل الجهد العالي.', + 'color': const Color(0xFF8B5CF6), + 'icon': CupertinoIcons.bolt_fill, + 'internalParts': [ + 'جسر الترانزستورات (IGBT / SiC Power Modules): 6 مفاتيح قدرة فائقة السرعة للتحويل بين DC و AC.', + 'مكثف التنعيم الرئيسي (DC Link Bulk Capacitor): تخميد التموجات وتثبيت الجهد الداخل من البطارية.', + 'ملفات العضو الساكن (Stator Windings): 3 أطوار من النحاس المجدول (U, V, W) لتوليد مجال مغناطيسي دوار.', + 'العضو الدوار (Permanent Magnet Rotor): دوار مغناطيسي دائم فائق العزم متصل بعمود التروس التخفيضية.', + 'حساس الموضع الزاوي (Resolver): يرسل زاوية الدوران بدقة متناهية إلى كمبيوتر المحول.', + ], + 'connectionsDetail': [ + 'المدخل: كابلات الجهد العالي البرتقالية (+ و -) القادمة من بطارية الجر.', + 'المخرج الحركي: 3 كابلات نحاسية معزولة متجهة مباشرة إلى أطوار المحرك (Phase U, V, W).', + 'دائرة التبريد المائي: خطوط سائل تبريد مائي جليكول لمنع ارتفاع حرارة ترانزستورات IGBT.', + 'وصلة CAN-Bus: إشارات الأوامر وعزم الدوران من دواسة التسارع وكمبيوتر القيادة (VCU).', + ], + 'inspectionProtocol': [ + 'الخطوة 1: افحص غياب الجهد (Zero Voltage Check) عند مدخل المحول بعد سحب قابس MSD.', + 'الخطوة 2: انتظر 5 دقائق لتفريغ شحنة المكثفات الداخلية قبل فتح الغطاء المعدني.', + 'الخطوة 3: اضبط الملتيميتر على قياس المقاومة المنخفضة (Milliohm / Resistance Ω).', + 'الخطوة 4: قِس المقاومة بين U-V، ثم V-W، ثم W-U. يجب أن تكون متطابقة تماماً (0.18 Ω ± 5%).', + ], + }, + { + 'id': 'service_plug', + 'title': 'قابس الأمان وفصل الخدمة اليدوي (Manual Service Disconnect - MSD)', + 'subtitle': 'مفتاح أمان ميكانيكي مدمج بفيوز عالي الجهد • يقسم حزمة البطارية لنصفين', + 'location': 'في الكونسول الوسطي بين المقاعد أو أسفل المقعد الخلفي لسهولة وصول الفني وفرق الإنقاذ والدفاع المدني.', + 'function': 'فصل الدائرة الكهربائية للبطارية ميكانيكياً إلى قسمين معزولين، وقطع حلقة الأمان (HVIL) لمنع تشغيل ريليهات التوصيل.', + 'connection': 'يوضع على التوالي في منتصف خلايا البطارية، ويحتوي على دبابيس حلقة القفل (Interlock Pins) التي تسبق فصل الجهد.', + 'howToInspect': 'عند سحب القابس، افحص استمرارية الفيوز الداخلي (Continuity Test) بين طرفي القابس للتأكد من عدم احتراقه.', + 'meterReading': '0.00', + 'meterUnit': 'Ω (استمرارية)', + 'meterMode': 'فحص الاتصال والفيوز', + 'probeRed': 'الطرف الأول للقابس (Pin 1)', + 'probeBlack': 'الطرف الثاني للقابس (Pin 2)', + 'normalRange': '0.00 Ω (توصيل تام) / ∞ (فيوز محترق)', + 'safetyStatus': 'الخطوة الأولى والأساسية قبل لمس أي جزء برتقالي في السيارة.', + 'color': const Color(0xFFF59E0B), + 'icon': CupertinoIcons.shield_fill, + 'internalParts': [ + 'مقبض الأمان ذو المرحلتين (Two-Stage Lever): يمنع السحب المفاجئ لضمان تفريغ التيار أولاً.', + 'فيوز الجهد العالي الداخلي (Fast-Acting Ceramic Fuse): صمام فائق السرعة 400A / 450V DC.', + 'شفرات التوصيل النحاسية العريضة (Main Copper Blades): نقل تيار الجهد العالي بين نصفي البطارية.', + 'دبابيس حلقة القفل (HVIL Interlock Pins): دبابيس قصيرة تفتح أولاً عند رفع المقبض لإخطار الـ BMS.', + ], + 'connectionsDetail': [ + 'التوصيل التسلسلي: يوضع في منتصف مصفوفة خلايا البطارية، فيقسم الـ 384V إلى شطرين آمنين (~192V).', + 'دائرة HVIL: يتصل بدائرة إشارة الأمان 12V المتصلة بريليهات SMR.', + ], + 'inspectionProtocol': [ + 'الخطوة 1: ارفع سقاطة القفل اليدوي، ثم ارفع الذراع 90 درجة، ثم اسحب القابس للخارج عمودياً.', + 'الخطوة 2: اضبط الملتيميتر على وضع فحص الاتصال والاستمرارية (Continuity / Diode Test 🔔).', + 'الخطوة 3: ضع المجس الأحمر على الطرف 1 والمجس الأسود على الطرف 2 لقابس الخدمة.', + 'الخطوة 4: إذا أصدر الجهاز رنيناً مستمراً (0.00 Ω) فالفيوز سليم؛ وإذا أعطى (OL / ∞) فالفيوز محترق.', + ], + }, + { + 'id': 'cables', + 'title': 'كابلات الجهد العالي البرتقالية (High-Voltage Orange Bus Lines)', + 'subtitle': 'كابلات معزولة بطبقة سيليكونية ومدرعة بشبكة تأريض ضد التشويش (EMC Shielding)', + 'location': 'تمتد محمية داخل قنوات فولاذية أسفل هيكل السيارة على طول الشاسيه.', + 'function': 'نقل تيار القدرة العالي (حتى 250 أمبير) بأقل فاقد حراري ممكن وبأعلى درجات العزل الكهربائي.', + 'connection': 'تربط القطبين الموجب والسالب للبطارية بمحول القدرة الأمامي وبمنفذ الشحن السريع.', + 'howToInspect': 'فحص مقاومة العزل (Megger / Isolation Test): بين الموصل النحاسي الداخلي وشاسيه السيارة (أرضي). يجب أن تكون > 0.5 MΩ.', + 'meterReading': '5.2', + 'meterUnit': 'MΩ', + 'meterMode': 'مقاومة عزل الشاسيه MΩ', + 'probeRed': 'موصل الكابل النحاسي الداخلي', + 'probeBlack': 'أرضي هيكل الشاسيه (Chassis GND)', + 'normalRange': '> 0.5 MΩ (مطابق لمعيار ISO 6469-1)', + 'safetyStatus': 'اللون البرتقالي عالمياً يعني خطر الجهد القاتل (> 60V DC). يمنع تجريحه أو ثقبه.', + 'color': const Color(0xFFFF7A00), + 'icon': CupertinoIcons.waveform_path, + 'internalParts': [ + 'القلب النحاسي (Multi-Strand Copper Conductor): موصل مرن يتحمل تيارات تصل إلى 250A مستمر.', + 'العازل الأولي (XLPE Silicone Dielectric): بوليمر سيليكوني عازل يتحمل 1000V DC وحرارة 150°C.', + 'درع الحماية الكهرومغناطيسية (EMC Braided Shielding): شبكة أسلاك قصديرية تحجب التشويش اللاسلكي.', + 'الغلاف الخارجي البرتقالي (Orange Protective Sheath): مقاوم للتآكل والزيوت باللون التحذيري الدولي RAL 2003.', + ], + 'connectionsDetail': [ + 'القناة السفلية للشاسيه: تمر الكابلات داخل مجارٍ معدنية لحمايتها من صدمات الطريق والحصى.', + 'نهايات الكابلات: كتل توصيل محكمة ومقفلة بحلقات مانعة لتسرب الرطوبة (IP67) مع مفاتيح تأريض بالشاسيه.', + ], + 'inspectionProtocol': [ + 'الخطوة 1: تأكد من عزل منظومة الجهد العالي وسحب قابس MSD وفحص 0.00V.', + 'الخطوة 2: استخدم جهاز قياس العزل الكهربائي (Megohmmeter / Megger) بجهد اختبار 500V DC.', + 'الخطوة 3: ضع المجس الأحمر على الموصل النحاسي الداخلي، وضع المجس الأسود على هيكل الشاسيه (Ground).', + 'الخطوة 4: القراءة السليمة يجب أن تكون أكبر من 0.5 MΩ (القراءة الطبيعية هنا 5.2 MΩ). أي قيمة أقل تعني تسريباً خطيراً.', + ], + }, + { + 'id': 'charging_port', + 'title': 'منفذ الشحن المزدوج (Combined AC / DC Fast Charging Inlet)', + 'subtitle': 'مقبس شحن موحد CCS2 متوافق مع محطات الشحن المنزلية والشواحن السريعة DC', + 'location': 'في الرفرف الخلفي الأيسر للسيارة مع غطاء محكم العزل ضد الماء والأتربة (IP67).', + 'function': 'استقبال التيار المتردد AC لشحن البطارية عبر الشاحن الداخلي، أو التيار المستمر DC الفائق للشحن السريع المباشر.', + 'connection': 'يرتبط بخطوط البطارية عبر قواطع حماية، وبخطوط إشارات الاتصال (CP: Control Pilot & PP: Proximity Pilot) بكمبيوتر السيارة.', + 'howToInspect': 'فحص مقاومة خط القرب (PP) والتأكد من إشارة تردد خط التوجيه (CP PWM 1kHz) للتأكد من جاهزية كابل الشاحن.', + 'meterReading': '220', + 'meterUnit': 'Ω', + 'meterMode': 'مقاومة إشارة القرب PP', + 'probeRed': 'دبوس إشارة القرب (Pin PP)', + 'probeBlack': 'دبوس الأرضي الوقائي (PE Pin)', + 'normalRange': '220 Ω (كابل 32A مشبوك وجاهز)', + 'safetyStatus': 'قفل الأمان الكهروميكانيكي يمنع سحب المقبس أثناء سريان تيار الشحن.', + 'color': const Color(0xFF00F5D4), + 'icon': CupertinoIcons.bolt_badge_a, + 'internalParts': [ + 'منفذ Type 2 العلوي: 3 خطوط متناوبة (L1, L2, L3) وخط محايد (N) للشحن البطيء والمنزلي حتى 22kW.', + 'خط التأريض الوقائي (PE Pin): أطول دبوس في المقبس لضمان تأريض السيارة قبل تدفق أي تيار.', + 'دبابيس الإشارة (CP & PP): إشارة التحكم بنبضات التردد 1kHz وإشارة القرب لمعرفة سعة كابل الشحن.', + 'منفذ التيار المستمر السفلي (DC Combo Pins): قطبان ضخمان (DC+ و DC-) للشحن السريع حتى 150kW.', + 'قفل الأمان الكهروميكانيكي (Actuator Solenoid): يقفل الكابل ميكانيكياً لمنع سحبه أثناء الشحن.', + ], + 'connectionsDetail': [ + 'خطوط القدرة: متصلة بالشاحن الداخلي (OBC) للـ AC، ومتصلة مباشرة بقواطع البطارية للـ DC.', + 'خطوط الإشارات: متصلة بوحدة التحكم في الشحن (CCU) وحاسوب المركبة VCU.', + ], + 'inspectionProtocol': [ + 'الخطوة 1: افحص منفذ الشحن بصرياً للتأكد من خلوه من الأتربة أو التآكل أو الرطوبة.', + 'الخطوة 2: اضبط الملتيميتر على قياس المقاومة (Resistance Ω).', + 'الخطوة 3: ضع المجس الأحمر على دبوس القرب (PP) والمجس الأسود على دبوس الأرضي (PE).', + 'الخطوة 4: عند إدخال كابل شحن 32A، يجب أن يقرأ الملتيميتر 220 Ω بدقة دليلاً على جاهزية دورة الشحن.', + ], + }, + { + 'id': 'aux_battery', + 'title': 'بطارية الـ 12V المساعدة ونظام التحكم (12V Auxiliary Battery & DC-DC)', + 'subtitle': 'تغذية كمبيوترات التحكم (ECUs)، ريليهات الأمان، الإنارة، وشاشات العدادات', + 'location': 'في الزاوية اليمنى لحوض المحرك الأمامي.', + 'function': 'تشغيل أنظمة السيارة المنخفضة والبدء في تشغيل كمبيوتر السيارة ومراقبة الأمان قبل السماح بتوصيل الجهد العالي.', + 'connection': 'تشحن من بطارية الجهد العالي عبر محول خفض الجهد (DC-DC Converter 384V -> 14V) بدلاً من الدينامو التقليدي.', + 'howToInspect': 'قياس جهد البطارية أثناء التشغيل بجهاز الملتيميتر على وضيعة DCV. يجب أن يقرأ بين 13.8V و 14.4V عند عمل محول DC-DC.', + 'meterReading': '14.2', + 'meterUnit': 'V DC', + 'meterMode': 'جهد مستمر منخفض DCV', + 'probeRed': 'القطب الموجب (+12V)', + 'probeBlack': 'القطب السالب (-12V / GND)', + 'normalRange': '12.4V - 12.8V (متوقفة) / 14.2V (شحن DC-DC)', + 'safetyStatus': 'جهد منخفض آمن للمس اليدوي المباشر.', + 'color': const Color(0xFF38BDF8), + 'icon': CupertinoIcons.car_detailed, + 'internalParts': [ + 'بطارية 12V المساعدة: بطارية AGM أو ليثيوم 12V لتشغيل الأضواء والوسائد الهوائية والشاشات.', + 'محول خفض الجهد (DC-DC Converter Module): يستبدل دينامو الشحن التقليدي ويخفض 384V إلى 14.2V.', + 'حاجز العزل الجلفاني (Galvanic Isolation Barrier): محول تردد عالي يفصل تماماً بين نظام الـ 384V ونظام الـ 12V.', + 'حساس التيار الذكي (IBS): يقيس استهلاك التيار وحالة شحن بطارية الـ 12V.', + ], + 'connectionsDetail': [ + 'المدخل العالي: يتصل مباشرة ببطارية الجهد العالي عبر فيوز مخصص.', + 'المخرج المنخفض: يتصل بقطب البطارية المساعدة وعلبة الفيوزات الرئيسية (12V Distribution Box).', + 'الأرضي المشترك: يتصل القطب السالب مباشرة بهيكل شاسيه السيارة لتوفير دائرة رجوع سالبة.', + ], + 'inspectionProtocol': [ + 'الخطوة 1: قِس الجهد على قطبي بطارية الـ 12V عندما تكون السيارة في وضع الإيقاف (يجب أن يكون 12.4V - 12.8V).', + 'الخطوة 2: ضع السيارة في وضع الجاهزية (Ready Mode) واقرأ الجهد مجدداً.', + 'الخطوة 3: إذا ارتفع الجهد إلى 13.8V - 14.4V، فمحول DC-DC يعمل بكفاءة ويقوم بشحن بطارية الـ 12V.', + 'الخطوة 4: إذا بقي الجهد منخفضاً (12.2V)، فمحول الـ DC-DC به عطل أو أن فيوز الجهد العالي المغذي له مقطوع.', + ], + }, + ]; + final List> _sectors = [ { 'title': 'المركبات والطاقة المتجددة', 'icon': Icons.electric_car, - 'color': Color(0xFF10B981), + 'color': const Color(0xFF10B981), 'tradesCount': 18, 'featured': 'ميكانيك وتشخيص المركبات الكهربائية والهجينة (EV)', 'description': 'فحص أنظمة الجهد العالي، بطاريات الليثيوم، ومحولات التيار (Inverters).', @@ -55,7 +272,7 @@ class _VocationalTrainingScreenState extends State { 'title': 'التكنولوجيا والتحول الرقمي', 'icon': CupertinoIcons.device_laptop, - 'color': Color(0xFF00F5D4), + 'color': const Color(0xFF00F5D4), 'tradesCount': 22, 'featured': 'أمن الشبكات والدعم الفني السحابي', 'description': 'هندسة الشبكات المحلية، الألياف الضوئية، وصيانة الخوادم والأنظمة الذكية.', @@ -70,7 +287,7 @@ class _VocationalTrainingScreenState extends State { 'title': 'الصناعات الهندسية والميكانيكية', 'icon': CupertinoIcons.wrench_fill, - 'color': Color(0xFFF59E0B), + 'color': const Color(0xFFF59E0B), 'tradesCount': 28, 'featured': 'اللحام وتشكيل المعادن المتقدم (TIG/MIG)', 'description': 'الخراطة، تشكيل المعادن، واللحام بالأرغون وخطوط الإنتاج المؤتمتة.', @@ -85,7 +302,7 @@ class _VocationalTrainingScreenState extends State { 'title': 'الضيافة والسياحة والفندقة', 'icon': CupertinoIcons.building_2_fill, - 'color': Color(0xFFEC4899), + 'color': const Color(0xFFEC4899), 'tradesCount': 16, 'featured': 'فنون الطهي وإنتاج الأغذية الفندقية', 'description': 'إدارة المكاتب الأمامية، فنون الطهي الدولي، وخدمة الغرف والضيافة.', @@ -99,7 +316,7 @@ class _VocationalTrainingScreenState extends State { 'title': 'الزراعة الحديثة والري الذكي', 'icon': CupertinoIcons.leaf_arrow_circlepath, - 'color': Color(0xFF22C55E), + 'color': const Color(0xFF22C55E), 'tradesCount': 14, 'featured': 'الزراعة المائية والبيوت البلاستيكية الذكية', 'description': 'أنظمة الزراعة بدون تربة (Hydroponics)، الري المحوسب، وتنسيق الحدائق.', @@ -113,7 +330,7 @@ class _VocationalTrainingScreenState extends State { 'title': 'الصناعات الدوائية والكيميائية', 'icon': Icons.science_outlined, - 'color': Color(0xFF8B5CF6), + 'color': const Color(0xFF8B5CF6), 'tradesCount': 12, 'featured': 'تشغيل خطوط التعبئة والتغليف الدوائي', 'description': 'تطبيق معايير ممارسات التصنيع الجيد (GMP) ومراقبة جودة الإنتاج الكيميائي.', @@ -126,7 +343,7 @@ class _VocationalTrainingScreenState extends State { 'title': 'الحلاقة والتجميل والعناية', 'icon': CupertinoIcons.scissors, - 'color': Color(0xFFF43F5E), + 'color': const Color(0xFFF43F5E), 'tradesCount': 15, 'featured': 'تصفيف الشعر والتجميل الاحترافي', 'description': 'تقنيات العناية بالبشرة، التصميم المسرحي، وإدارة الصالونات الحديثة.', @@ -139,7 +356,7 @@ class _VocationalTrainingScreenState extends State { 'title': 'الحرف التقليدية والمشغولات اليدوية', 'icon': CupertinoIcons.cube_box_fill, - 'color': Color(0xFFD97706), + 'color': const Color(0xFFD97706), 'tradesCount': 15, 'featured': 'صياغة الذهب والمجوهرات والفسيفساء', 'description': 'إحياء التراث الحرفي الأردني، فن الفسيفساء المادبي، وحفر الخشب التراثي.', @@ -167,26 +384,6 @@ class _VocationalTrainingScreenState extends State super.dispose(); } - void _triggerFault(String faultCode, String description, double rIso, bool hvilState) { - setState(() { - _activeDtcCode = '$faultCode: $description'; - _isolationResistance = rIso; - _hvilInterlockClosed = hvilState; - if (!hvilState || rIso < 0.5) { - _contactorPrechargeActive = false; - } - }); - } - - void _clearFaults() { - setState(() { - _activeDtcCode = 'P0000: No Faults Detected (Normal Operation)'; - _isolationResistance = 5.2; - _hvilInterlockClosed = true; - _contactorPrechargeActive = true; - }); - } - @override Widget build(BuildContext context) { final activeSector = _sectors[_selectedSectorIndex]; @@ -528,21 +725,94 @@ class _VocationalTrainingScreenState extends State ); } + void _toggleServicePlug() { + setState(() { + _servicePlugRemoved = !_servicePlugRemoved; + if (_servicePlugRemoved) { + _hvilInterlockClosed = false; + _packVoltage = 0.0; + _activeDtcCode = 'P0A0D: HVIL Interlock Open (Service Plug Removed - Safe Mode)'; + } else { + _hvilInterlockClosed = true; + _packVoltage = 384.0; + _isolationResistance = 5.2; + _activeDtcCode = 'P0000: No Faults Detected (Normal Operation)'; + } + }); + } + + void _triggerFault(String code, String desc, double isolation, bool dropVoltage) { + setState(() { + _activeDtcCode = '$code: $desc'; + _isolationResistance = isolation; + if (dropVoltage) { + _packVoltage = 0.0; + _hvilInterlockClosed = false; + } + }); + } + + void _clearFaults() { + setState(() { + _activeDtcCode = 'P0000: No Faults Detected (Normal Operation)'; + _servicePlugRemoved = false; + _isolationResistance = 5.2; + _packVoltage = 384.0; + _hvilInterlockClosed = true; + }); + } + // =========================================================================== - // TAB 2: INTERACTIVE EV & HYBRID DIAGNOSTIC VIRTUAL LAB (120 FPS) + // TAB 2: INTERACTIVE EV & HYBRID CAR ARCHITECTURE & MULTIMETER LAB // =========================================================================== Widget _buildEvDiagnosticLabTab() { + final activeComp = _carComponents[_selectedComponentIndex]; + return ListView( padding: const EdgeInsets.all(16), children: [ - // Live Diagnostics Canvas (Battery Pack & Inverter) + // Section Header + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF10B981).withAlpha(25), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFF10B981).withAlpha(80)), + ), + child: const Icon(Icons.electric_car, color: Color(0xFF10B981), size: 20), + ), + const SizedBox(width: 10), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'مخطط هيكل السيارة الكهربائية وتنظيم القطاعات', + style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w800), + ), + Text( + 'اضغط على أي قطاع في السيارة لمعرفة كيف يعمل وكيف تفحصه بالعداد الرقمي', + style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + + // Live Car Chassis Blueprint Canvas Container( - height: 280, + height: 290, decoration: BoxDecoration( - color: const Color(0xFF070E14), + color: const Color(0xFF070F16), borderRadius: BorderRadius.circular(20), border: Border.all( - color: _activeDtcCode.startsWith('P0000') ? const Color(0xFF10B981).withAlpha(80) : const Color(0xFFFF2D55), + color: _servicePlugRemoved + ? const Color(0xFF10B981) + : (_activeDtcCode.startsWith('P0000') ? const Color(0xFF00F5D4).withAlpha(100) : const Color(0xFFFF2D55)), width: 1.5, ), boxShadow: const [ @@ -552,53 +822,432 @@ class _VocationalTrainingScreenState extends State child: ClipRRect( borderRadius: BorderRadius.circular(19), child: CustomPaint( - painter: _EvDiagnosticsPainter( + painter: _CarChassisPainter( + selectedComponentIndex: _selectedComponentIndex, + servicePlugRemoved: _servicePlugRemoved, batterySoc: _batterySoc, packVoltage: _packVoltage, - isolationResistance: _isolationResistance, - hvilClosed: _hvilInterlockClosed, isFaulted: !_activeDtcCode.startsWith('P0000'), ), ), ), ), - const SizedBox(height: 14), + const SizedBox(height: 10), - // Live Diagnostic Telemetry Grid + // Live Diagnostic Telemetry Bar Row( children: [ - _buildTelemetryCard( - 'حالة الشحن (SoC)', - '${_batterySoc.toInt()}%', - const Color(0xFF10B981), - CupertinoIcons.battery_full, - ), - const SizedBox(width: 8), - _buildTelemetryCard( - 'جهد البطارية (Pack V)', - '${_packVoltage.toInt()} V', - AppColors.saqelCyan, - CupertinoIcons.bolt_fill, - ), - const SizedBox(width: 8), - _buildTelemetryCard( - 'مقاومة العزل (R_iso)', - '${_isolationResistance.toStringAsFixed(1)} MΩ', - _isolationResistance >= 0.5 ? const Color(0xFF10B981) : const Color(0xFFFF2D55), - CupertinoIcons.shield_lefthalf_fill, - ), - const SizedBox(width: 8), - _buildTelemetryCard( - 'حلقة القفل (HVIL)', - _hvilInterlockClosed ? 'مغلقة (آمن)' : 'مفتوحة (خطر)', - _hvilInterlockClosed ? const Color(0xFF10B981) : const Color(0xFFFF2D55), - CupertinoIcons.shield_fill, - ), + _buildCarTelemetryBadge('الشحن (SoC)', '${_batterySoc.toInt()}%', const Color(0xFF10B981), CupertinoIcons.battery_full), + const SizedBox(width: 6), + _buildCarTelemetryBadge('جهد الكابلات', '${_packVoltage.toInt()} V', _packVoltage > 0 ? const Color(0xFFFF7A00) : const Color(0xFF10B981), CupertinoIcons.bolt_fill), + const SizedBox(width: 6), + _buildCarTelemetryBadge('مقاومة العزل', '${_isolationResistance.toStringAsFixed(1)} MΩ', _isolationResistance >= 0.5 ? const Color(0xFF10B981) : const Color(0xFFFF2D55), CupertinoIcons.shield_lefthalf_fill), + const SizedBox(width: 6), + _buildCarTelemetryBadge('حلقة القفل HVIL', _hvilInterlockClosed ? 'مغلقة' : 'مفتوحة', _hvilInterlockClosed ? const Color(0xFF10B981) : const Color(0xFFFF2D55), CupertinoIcons.shield_fill), ], ), + const SizedBox(height: 12), + + // Component Quick Selector Pills + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: _carComponents.asMap().entries.map((entry) { + final idx = entry.key; + final comp = entry.value; + final isSel = _selectedComponentIndex == idx; + final Color compColor = comp['color'] as Color; + + return Padding( + padding: const EdgeInsets.only(left: 8), + child: ChoiceChip( + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(comp['icon'] as IconData, size: 14, color: isSel ? Colors.black : compColor), + const SizedBox(width: 6), + Text(comp['title'].toString().split('(').first.trim()), + ], + ), + selected: isSel, + selectedColor: compColor, + backgroundColor: AppColors.darkSurface, + labelStyle: TextStyle( + color: isSel ? Colors.black : Colors.white70, + fontWeight: isSel ? FontWeight.w800 : FontWeight.w600, + fontSize: 11.5, + ), + onSelected: (selected) { + if (selected) setState(() => _selectedComponentIndex = idx); + }, + ), + ); + }).toList(), + ), + ), const SizedBox(height: 14), - // Active DTC Code Box + // Digital Multimeter Inspection Simulator (عداد الفحص الرقمي) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF131D24), Color(0xFF0A1218)], + begin: Alignment.topRight, + end: Alignment.bottomLeft, + ), + borderRadius: BorderRadius.circular(18), + border: Border.all(color: const Color(0xFFF59E0B).withAlpha(120), width: 1.5), + boxShadow: const [ + BoxShadow(color: Colors.black45, blurRadius: 10, offset: Offset(0, 4)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFF59E0B).withAlpha(30), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(CupertinoIcons.speedometer, color: Color(0xFFF59E0B), size: 18), + ), + const SizedBox(width: 8), + const Expanded( + child: Text( + 'جهاز الفحص الرقمي (Digital Multimeter)', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13.5), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.white24), + ), + child: Text( + activeComp['meterMode'] as String, + style: const TextStyle(color: Color(0xFF00F5D4), fontSize: 10.5, fontWeight: FontWeight.bold), + ), + ), + ], + ), + const SizedBox(height: 12), + + // High-Contrast OLED LCD Screen + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), + decoration: BoxDecoration( + color: const Color(0xFF03080C), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFF00F5D4).withAlpha(80)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'نقطة القياس: ${activeComp['title'].toString().split('(').first}', + style: const TextStyle(color: Colors.white60, fontSize: 11), + ), + const SizedBox(height: 2), + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + _servicePlugRemoved && activeComp['id'] == 'battery' + ? '0.00' + : (activeComp['id'] == 'battery' ? _packVoltage.toStringAsFixed(1) : activeComp['meterReading'] as String), + style: TextStyle( + fontFamily: 'Courier', + color: _servicePlugRemoved && activeComp['id'] == 'battery' + ? const Color(0xFF10B981) + : const Color(0xFF00F5D4), + fontSize: 32, + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(width: 8), + Text( + activeComp['meterUnit'] as String, + style: const TextStyle(color: Color(0xFFF59E0B), fontSize: 15, fontWeight: FontWeight.bold), + ), + ], + ), + ], + ), + // Probes indicator + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + children: [ + const Text('المجس الأحمر (+): ', style: TextStyle(color: Colors.white70, fontSize: 10)), + Container(width: 8, height: 8, decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle)), + ], + ), + Text(activeComp['probeRed'] as String, style: const TextStyle(color: Colors.redAccent, fontSize: 10.5, fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + Row( + children: [ + const Text('المجس الأسود (-): ', style: TextStyle(color: Colors.white70, fontSize: 10)), + Container(width: 8, height: 8, decoration: const BoxDecoration(color: Colors.black, shape: BoxShape.circle)), + ], + ), + Text(activeComp['probeBlack'] as String, style: const TextStyle(color: Colors.white70, fontSize: 10.5, fontWeight: FontWeight.bold)), + ], + ), + ], + ), + ), + const SizedBox(height: 10), + + // Status Pill & Verification Note + Row( + children: [ + Icon( + _servicePlugRemoved ? CupertinoIcons.checkmark_shield_fill : CupertinoIcons.bolt_fill, + size: 14, + color: _servicePlugRemoved ? const Color(0xFF10B981) : const Color(0xFFFF2D55), + ), + const SizedBox(width: 6), + Expanded( + child: Text( + _servicePlugRemoved + ? 'خلو الجهد مؤكد (Zero Voltage Verified) • النظام آمن تماماً للصيانة بدون صعق 🛡️' + : 'تنبيه: ${activeComp['safetyStatus']}', + style: TextStyle( + color: _servicePlugRemoved ? const Color(0xFF10B981) : const Color(0xFFFF7A00), + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 12), + + // Service Plug Safety Action Button + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton.icon( + icon: Icon( + _servicePlugRemoved ? CupertinoIcons.bolt_fill : CupertinoIcons.shield_fill, + size: 18, + ), + label: Text( + _servicePlugRemoved + ? 'إعادة تركيب وتثبيت قابس الأمان ⚡ (إعادة تشغيل النظام)' + : 'نزع قابس الأمان يدويّاً 🛑 (عزل الجهد والتحقق من 0.00V)', + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w800), + ), + style: ElevatedButton.styleFrom( + backgroundColor: _servicePlugRemoved ? const Color(0xFF10B981) : const Color(0xFFF59E0B), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + ), + onPressed: _toggleServicePlug, + ), + ), + const SizedBox(height: 14), + + // Component Anatomy & Explanatory Card (تنظيم القطاع وعمله) + LuxuryCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(activeComp['icon'] as IconData, color: activeComp['color'] as Color, size: 20), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + activeComp['title'] as String, + style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w800), + ), + Text( + activeComp['subtitle'] as String, + style: TextStyle(color: activeComp['color'] as Color, fontSize: 11, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + + // Component Detail Tabs + Container( + decoration: BoxDecoration( + color: const Color(0xFF070F16), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white12), + ), + child: Row( + children: [ + _buildComponentTabItem(0, 'تشريح القطاع والرسم 🔬'), + _buildComponentTabItem(1, 'طريقة الربط والشبك 🔗'), + _buildComponentTabItem(2, 'الفحص وقراءة العداد 📟'), + ], + ), + ), + const SizedBox(height: 12), + + // Dedicated Isolated Component Blueprint Canvas (رسمة القطاع المنفصل) + Container( + height: 230, + width: double.infinity, + decoration: BoxDecoration( + color: const Color(0xFF050B10), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: (activeComp['color'] as Color).withAlpha(100), width: 1.5), + boxShadow: const [ + BoxShadow(color: Colors.black45, blurRadius: 12, offset: Offset(0, 4)), + ], + ), + child: Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(15), + child: CustomPaint( + size: const Size(double.infinity, 230), + painter: _ComponentAnatomyPainter( + componentId: activeComp['id'] as String, + primaryColor: activeComp['color'] as Color, + servicePlugRemoved: _servicePlugRemoved, + probesPlaced: _probesPlaced, + meterReading: _servicePlugRemoved && activeComp['id'] == 'battery' + ? '0.00' + : (activeComp['id'] == 'battery' ? _packVoltage.toStringAsFixed(1) : activeComp['meterReading'] as String), + meterUnit: activeComp['meterUnit'] as String, + pulseValue: _pulseController.value, + ), + ), + ), + // Probes toggle pill + Positioned( + top: 8, + left: 8, + child: GestureDetector( + onTap: () => setState(() => _probesPlaced = !_probesPlaced), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: _probesPlaced ? const Color(0xFF10B981).withAlpha(40) : Colors.black87, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: _probesPlaced ? const Color(0xFF10B981) : Colors.white24), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _probesPlaced ? CupertinoIcons.check_mark_circled_solid : CupertinoIcons.circle, + size: 13, + color: _probesPlaced ? const Color(0xFF10B981) : Colors.white60, + ), + const SizedBox(width: 5), + Text( + _probesPlaced ? 'المجسات متصلة بالقطعة' : 'المجسات مرفوعة', + style: TextStyle( + color: _probesPlaced ? const Color(0xFF10B981) : Colors.white70, + fontSize: 10.5, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 14), + + // Dynamic Tab Body Content + if (_componentDetailTab == 0) ...[ + _buildAnatomyPoint('📍 الموقع في هيكل السيارة:', activeComp['location'] as String), + const SizedBox(height: 10), + _buildAnatomyPoint('⚙️ الوظيفة الأساسية في المنظومة:', activeComp['function'] as String), + const SizedBox(height: 10), + const Text( + '🔬 تنظيم القطاع والقطع الداخلية:', + style: TextStyle(color: AppColors.saqelCyan, fontSize: 11.5, fontWeight: FontWeight.w700), + ), + const SizedBox(height: 6), + if (activeComp['internalParts'] != null) + ...((activeComp['internalParts'] as List).map((p) => _buildBulletPoint(p))) + else + _buildBulletPoint(activeComp['subtitle'] as String), + ] else if (_componentDetailTab == 1) ...[ + _buildAnatomyPoint('🔗 مسار التوصيل والشبك بالسيارة:', activeComp['connection'] as String), + const SizedBox(height: 10), + const Text( + '⚡ خطوط القدرة وحلقات الأمان المترابطة:', + style: TextStyle(color: AppColors.saqelCyan, fontSize: 11.5, fontWeight: FontWeight.w700), + ), + const SizedBox(height: 6), + if (activeComp['connectionsDetail'] != null) + ...((activeComp['connectionsDetail'] as List).map((c) => _buildBulletPoint(c))) + else + _buildBulletPoint(activeComp['connection'] as String), + ] else ...[ + _buildAnatomyPoint('🔍 طريقة الفحص بالملتيميتر خطوة بخطوة:', activeComp['howToInspect'] as String), + const SizedBox(height: 10), + const Text( + '📋 بروتوكول القياس والفحص المخبري:', + style: TextStyle(color: AppColors.saqelCyan, fontSize: 11.5, fontWeight: FontWeight.w700), + ), + const SizedBox(height: 6), + if (activeComp['inspectionProtocol'] != null) + ...((activeComp['inspectionProtocol'] as List).map((s) => _buildBulletPoint(s))), + const SizedBox(height: 6), + _buildAnatomyPoint('📊 القيمة المعيارية المعتمدة:', activeComp['normalRange'] as String), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFF1E1408), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFFF59E0B).withAlpha(120)), + ), + child: Row( + children: [ + const Icon(CupertinoIcons.shield_lefthalf_fill, color: Color(0xFFF59E0B), size: 16), + const SizedBox(width: 8), + Expanded( + child: Text( + 'السلامة المهنية: ${activeComp['safetyStatus']}', + style: const TextStyle(color: Color(0xFFFFD166), fontSize: 11, fontWeight: FontWeight.w600), + ), + ), + ], + ), + ), + ], + ], + ), + ), + const SizedBox(height: 14), + + // Active DTC Box Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( @@ -627,7 +1276,7 @@ class _VocationalTrainingScreenState extends State style: TextStyle( color: _activeDtcCode.startsWith('P0000') ? const Color(0xFF10B981) : const Color(0xFFFF2D55), fontWeight: FontWeight.w800, - fontSize: 13, + fontSize: 12, ), ), ], @@ -636,7 +1285,7 @@ class _VocationalTrainingScreenState extends State ], ), ), - const SizedBox(height: 14), + const SizedBox(height: 12), // Simulated Fault Injection Controls LuxuryCard( @@ -679,22 +1328,85 @@ class _VocationalTrainingScreenState extends State ); } - Widget _buildTelemetryCard(String label, String value, Color color, IconData icon) { + Widget _buildComponentTabItem(int index, String title) { + final isSelected = _componentDetailTab == index; + return Expanded( + child: GestureDetector( + onTap: () => setState(() => _componentDetailTab = index), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF00F5D4).withAlpha(35) : Colors.transparent, + borderRadius: BorderRadius.circular(10), + border: isSelected ? Border.all(color: const Color(0xFF00F5D4), width: 1.2) : null, + ), + alignment: Alignment.center, + child: Text( + title, + style: TextStyle( + color: isSelected ? const Color(0xFF00F5D4) : Colors.white60, + fontSize: 11, + fontWeight: isSelected ? FontWeight.w800 : FontWeight.w600, + ), + ), + ), + ), + ); + } + + Widget _buildBulletPoint(String text) { + return Padding( + padding: const EdgeInsets.only(bottom: 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.only(top: 5, left: 8), + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Color(0xFF00F5D4), + shape: BoxShape.circle, + ), + ), + Expanded( + child: Text( + text, + style: const TextStyle(color: Colors.white, fontSize: 12, height: 1.45), + ), + ), + ], + ), + ); + } + + Widget _buildAnatomyPoint(String title, String content) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(color: AppColors.saqelCyan, fontSize: 11.5, fontWeight: FontWeight.w700)), + const SizedBox(height: 2), + Text(content, style: const TextStyle(color: Colors.white, fontSize: 12, height: 1.45)), + ], + ); + } + + Widget _buildCarTelemetryBadge(String label, String value, Color color, IconData icon) { return Expanded( child: Container( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6), decoration: BoxDecoration( color: AppColors.darkSurface, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: AppColors.darkCardBorder), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: color.withAlpha(80)), ), child: Column( children: [ - Icon(icon, color: color, size: 18), - const SizedBox(height: 4), - Text(value, style: TextStyle(color: color, fontWeight: FontWeight.w800, fontSize: 13.5)), + Icon(icon, color: color, size: 14), const SizedBox(height: 2), - Text(label, style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 9.5), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis), + Text(value, style: TextStyle(color: color, fontWeight: FontWeight.w800, fontSize: 11)), + const SizedBox(height: 1), + Text(label, style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 8.5), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis), ], ), ), @@ -830,20 +1542,20 @@ class _VocationalTrainingScreenState extends State } // ============================================================================= -// EV DIAGNOSTICS CUSTOM PAINTER (120 FPS HIGH-VOLTAGE ARCHITECTURE) +// REALISTIC CAR CHASSIS & COMPONENT ARCHITECTURE PAINTER // ============================================================================= -class _EvDiagnosticsPainter extends CustomPainter { +class _CarChassisPainter extends CustomPainter { + final int selectedComponentIndex; + final bool servicePlugRemoved; final double batterySoc; final double packVoltage; - final double isolationResistance; - final bool hvilClosed; final bool isFaulted; - _EvDiagnosticsPainter({ + _CarChassisPainter({ + required this.selectedComponentIndex, + required this.servicePlugRemoved, required this.batterySoc, required this.packVoltage, - required this.isolationResistance, - required this.hvilClosed, required this.isFaulted, }); @@ -853,82 +1565,763 @@ class _EvDiagnosticsPainter extends CustomPainter { final h = size.height; final cy = h / 2; - // Watermark - const watermarkSpan = TextSpan( - text: 'منصة صَقِل • مختبر فحص المركبات الكهربائية VTC EV Lab', - style: TextStyle(color: Color(0x3300F5D4), fontSize: 11, fontWeight: FontWeight.bold), - ); - final watermarkPainter = TextPainter(text: watermarkSpan, textDirection: TextDirection.rtl)..layout(); - watermarkPainter.paint(canvas, const Offset(12, 12)); + // Technical Workshop Grid + final gridPaint = Paint() + ..color = Colors.white.withAlpha(8) + ..strokeWidth = 1.0; + for (double x = 0; x < w; x += 24) { + canvas.drawLine(Offset(x, 0), Offset(x, h), gridPaint); + } + for (double y = 0; y < h; y += 24) { + canvas.drawLine(Offset(0, y), Offset(w, y), gridPaint); + } - // 1. High Voltage Battery Pack Silhouette - final bRect = RRect.fromRectAndRadius( - Rect.fromLTWH(24, cy - 65, w * 0.40, 130), - const Radius.circular(16), - ); - canvas.drawRRect(bRect, Paint()..color = const Color(0xFF0F1E29)); - canvas.drawRRect(bRect, Paint()..color = const Color(0xFF10B981)..style = PaintingStyle.stroke..strokeWidth = 2); + // Car Silhouette (Top View) + // Left side is FRONT of the car, Right side is REAR of the car + final carLeft = w * 0.10; + final carRight = w * 0.90; + final carWidth = carRight - carLeft; + final carTop = cy - 85; + final carBottom = cy + 85; - // Battery Pack label - const bSpan = TextSpan( - text: 'بطارية الجهد العالي (384V DC)\nLi-Ion NMC Battery Pack', - style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold), - ); - final btp = TextPainter(text: bSpan, textDirection: TextDirection.rtl)..layout(); - btp.paint(canvas, Offset(36, cy - 45)); + // 1. Four Wheels (Tires) + final tirePaint = Paint()..color = const Color(0xFF1E293B); + final rimPaint = Paint() + ..color = Colors.white38 + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; - // SoC Bar inside pack - final socW = (w * 0.40 - 24) * (batterySoc / 100.0); + void drawWheel(double x, double y) { + final wheelRect = RRect.fromRectAndRadius(Rect.fromLTWH(x - 14, y - 10, 28, 20), const Radius.circular(5)); + canvas.drawRRect(wheelRect, tirePaint); + canvas.drawRRect(wheelRect, rimPaint); + } + + // Front Wheels + drawWheel(carLeft + carWidth * 0.20, carTop - 4); + drawWheel(carLeft + carWidth * 0.20, carBottom + 4); + // Rear Wheels + drawWheel(carLeft + carWidth * 0.78, carTop - 4); + drawWheel(carLeft + carWidth * 0.78, carBottom + 4); + + // Front & Rear Axles + final axlePaint = Paint() + ..color = Colors.white24 + ..strokeWidth = 2.0; + canvas.drawLine(Offset(carLeft + carWidth * 0.20, carTop + 10), Offset(carLeft + carWidth * 0.20, carBottom - 10), axlePaint); + canvas.drawLine(Offset(carLeft + carWidth * 0.78, carTop + 10), Offset(carLeft + carWidth * 0.78, carBottom - 10), axlePaint); + + // 2. Car Body Shell Outline + final bodyPath = Path(); + bodyPath.moveTo(carLeft + 25, carTop + 8); + // Front hood curve + bodyPath.quadraticBezierTo(carLeft + 2, cy, carLeft + 25, carBottom - 8); + // Left side & wheels cutouts + bodyPath.lineTo(carLeft + carWidth * 0.88, carBottom - 8); + // Rear bumper curve + bodyPath.quadraticBezierTo(carRight, cy, carLeft + carWidth * 0.88, carTop + 8); + bodyPath.close(); + + // Fill body shell with subtle metallic tint + canvas.drawPath(bodyPath, Paint()..color = const Color(0xFF0C1924)); + canvas.drawPath( + bodyPath, + Paint() + ..color = const Color(0xFF00F5D4).withAlpha(50) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5, + ); + + // Windshield & Cockpit + final glassPaint = Paint() + ..color = const Color(0xFF1A3348).withAlpha(120) + ..style = PaintingStyle.fill; + final glassStroke = Paint() + ..color = const Color(0xFF38BDF8).withAlpha(60) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.0; + + // Front windshield + final windshieldPath = Path() + ..moveTo(carLeft + carWidth * 0.32, carTop + 16) + ..lineTo(carLeft + carWidth * 0.40, carTop + 24) + ..lineTo(carLeft + carWidth * 0.40, carBottom - 24) + ..lineTo(carLeft + carWidth * 0.32, carBottom - 16) + ..close(); + canvas.drawPath(windshieldPath, glassPaint); + canvas.drawPath(windshieldPath, glassStroke); + + // Rear windshield + final rearGlassPath = Path() + ..moveTo(carLeft + carWidth * 0.68, carTop + 24) + ..lineTo(carLeft + carWidth * 0.74, carTop + 16) + ..lineTo(carLeft + carWidth * 0.74, carBottom - 16) + ..lineTo(carLeft + carWidth * 0.68, carBottom - 24) + ..close(); + canvas.drawPath(rearGlassPath, glassPaint); + canvas.drawPath(rearGlassPath, glassStroke); + + // ========================================================================= + // 3. INTERNAL HIGH-VOLTAGE ARCHITECTURE (القطع الداخلية للسيارة) + // ========================================================================= + + // A. Traction Battery Pack (Floorpan) - Index 0 + final battRect = RRect.fromRectAndRadius( + Rect.fromLTWH(carLeft + carWidth * 0.38, cy - 48, carWidth * 0.32, 96), + const Radius.circular(10), + ); + final isBattSel = selectedComponentIndex == 0; + canvas.drawRRect(battRect, Paint()..color = const Color(0xFF0A221E)); canvas.drawRRect( - RRect.fromRectAndRadius(Rect.fromLTWH(36, cy + 20, socW, 16), const Radius.circular(6)), - Paint()..color = const Color(0xFF10B981), + battRect, + Paint() + ..color = isBattSel ? const Color(0xFF00F5D4) : const Color(0xFF10B981) + ..style = PaintingStyle.stroke + ..strokeWidth = isBattSel ? 3.0 : 1.5, ); - // 2. Inverter & Motor Unit + // Battery modules inside + final modulePaint = Paint()..color = const Color(0xFF10B981).withAlpha(40); + for (int m = 0; m < 4; m++) { + final mx = carLeft + carWidth * 0.40 + (m * (carWidth * 0.32 - 16) / 4); + canvas.drawRRect( + RRect.fromRectAndRadius(Rect.fromLTWH(mx, cy - 42, (carWidth * 0.32 - 24) / 4, 84), const Radius.circular(4)), + modulePaint, + ); + } + + // B. Inverter & Motor Unit (Front Axle) - Index 1 final invRect = RRect.fromRectAndRadius( - Rect.fromLTWH(w * 0.60, cy - 65, w * 0.34, 130), - const Radius.circular(16), + Rect.fromLTWH(carLeft + carWidth * 0.12, cy - 36, carWidth * 0.18, 72), + const Radius.circular(10), ); - canvas.drawRRect(invRect, Paint()..color = const Color(0xFF1E1A2E)); + final isInvSel = selectedComponentIndex == 1; + canvas.drawRRect(invRect, Paint()..color = const Color(0xFF1F1A36)); canvas.drawRRect( invRect, Paint() - ..color = isFaulted ? const Color(0xFFFF2D55) : const Color(0xFF8B5CF6) + ..color = isInvSel ? const Color(0xFF00F5D4) : const Color(0xFF8B5CF6) ..style = PaintingStyle.stroke - ..strokeWidth = 2, + ..strokeWidth = isInvSel ? 3.0 : 1.5, ); - final invSpan = TextSpan( - text: 'محول القدرة والمحرك (Inverter)\nAC 3-Phase Permanent Magnet', - style: TextStyle(color: isFaulted ? const Color(0xFFFF2D55) : Colors.white, fontSize: 11, fontWeight: FontWeight.bold), + // C. High Voltage Orange Bus Cables - Index 3 + final isCablesSel = selectedComponentIndex == 3; + final Color hvCableColor = servicePlugRemoved + ? Colors.grey.shade700 + : (isFaulted ? const Color(0xFFFF2D55) : const Color(0xFFFF7A00)); + final cablePaint = Paint() + ..color = hvCableColor + ..strokeWidth = isCablesSel ? 4.5 : 3.0 + ..strokeCap = StrokeCap.round; + + // +HV (Upper) and -HV (Lower) cables from Battery to Inverter + canvas.drawLine( + Offset(carLeft + carWidth * 0.38, cy - 18), + Offset(carLeft + carWidth * 0.30, cy - 18), + cablePaint, ); - final itp = TextPainter(text: invSpan, textDirection: TextDirection.rtl)..layout(); - itp.paint(canvas, Offset(w * 0.60 + 12, cy - 45)); - - // 3. High-Voltage Bus Cables (Orange Cables +HV and -HV) - final hvCableColor = isFaulted ? const Color(0xFFFF2D55) : const Color(0xFFFF7A00); // Standard Orange EV Cable - final cablePaint = Paint()..color = hvCableColor..strokeWidth = 4..strokeCap = StrokeCap.round; - - // +HV Cable (Top) - canvas.drawLine(Offset(24 + w * 0.40, cy - 20), Offset(w * 0.60, cy - 20), cablePaint); - // -HV Cable (Bottom) - canvas.drawLine(Offset(24 + w * 0.40, cy + 20), Offset(w * 0.60, cy + 20), cablePaint); - - // Cable Voltage Badge - final voltSpan = TextSpan( - text: isFaulted ? '⚠️ جهد معزول تلقائياً' : '+384V DC Bus', - style: TextStyle(color: hvCableColor, fontSize: 10, fontWeight: FontWeight.w800), + canvas.drawLine( + Offset(carLeft + carWidth * 0.38, cy + 18), + Offset(carLeft + carWidth * 0.30, cy + 18), + cablePaint, ); - final vtp = TextPainter(text: voltSpan, textDirection: TextDirection.ltr)..layout(); - vtp.paint(canvas, Offset(w * 0.46, cy - 36)); - // 4. HVIL Interlock Loop Indicator - final hvilColor = hvilClosed ? const Color(0xFF10B981) : const Color(0xFFFF2D55); - final hvilPaint = Paint()..color = hvilColor..strokeWidth = 2..style = PaintingStyle.stroke; - canvas.drawCircle(Offset(w * 0.50, cy + 45), 8, hvilPaint); - canvas.drawCircle(Offset(w * 0.50, cy + 45), 4, Paint()..color = hvilColor); + // Cable to Rear Charging Port + canvas.drawLine( + Offset(carLeft + carWidth * 0.70, cy - 30), + Offset(carLeft + carWidth * 0.82, cy - 50), + cablePaint, + ); + + // D. Manual Service Disconnect Plug (Center Console) - Index 2 + final isPlugSel = selectedComponentIndex == 2; + final plugCenter = Offset(carLeft + carWidth * 0.54, cy); + final plugColor = servicePlugRemoved ? const Color(0xFFFF2D55) : const Color(0xFFF59E0B); + canvas.drawCircle(plugCenter, 14, Paint()..color = const Color(0xFF261D10)); + canvas.drawCircle( + plugCenter, + 14, + Paint() + ..color = isPlugSel ? const Color(0xFF00F5D4) : plugColor + ..style = PaintingStyle.stroke + ..strokeWidth = isPlugSel ? 3.0 : 2.0, + ); + canvas.drawCircle(plugCenter, 7, Paint()..color = plugColor); + + // E. Combined Charging Port (Rear Fender) - Index 4 + final isPortSel = selectedComponentIndex == 4; + final portCenter = Offset(carLeft + carWidth * 0.82, carTop + 14); + canvas.drawCircle(portCenter, 11, Paint()..color = const Color(0xFF082224)); + canvas.drawCircle( + portCenter, + 11, + Paint() + ..color = isPortSel ? const Color(0xFF00F5D4) : const Color(0xFF00F5D4).withAlpha(120) + ..style = PaintingStyle.stroke + ..strokeWidth = isPortSel ? 2.5 : 1.5, + ); + + // F. 12V Auxiliary Battery (Front Corner) - Index 5 + final isAuxSel = selectedComponentIndex == 5; + final auxRect = RRect.fromRectAndRadius( + Rect.fromLTWH(carLeft + carWidth * 0.14, carBottom - 38, 26, 22), + const Radius.circular(5), + ); + canvas.drawRRect(auxRect, Paint()..color = const Color(0xFF0B2538)); + canvas.drawRRect( + auxRect, + Paint() + ..color = isAuxSel ? const Color(0xFF00F5D4) : const Color(0xFF38BDF8) + ..style = PaintingStyle.stroke + ..strokeWidth = isAuxSel ? 2.5 : 1.5, + ); + + // ========================================================================= + // 4. LABELS & HUD OVERLAY + // ========================================================================= + void drawHudText(String text, Offset pos, Color color, {bool isLtr = false}) { + final tp = TextPainter( + text: TextSpan( + text: text, + style: TextStyle(color: color, fontSize: 10, fontWeight: FontWeight.bold), + ), + textDirection: isLtr ? TextDirection.ltr : TextDirection.rtl, + )..layout(); + tp.paint(canvas, pos); + } + + drawHudText('المحرك والمحول (Inverter)', Offset(carLeft + carWidth * 0.10, cy - 54), const Color(0xFF8B5CF6)); + drawHudText( + servicePlugRemoved ? 'قابس الأمان (مفصول 🛑)' : 'قابس الأمان MSD 🛑', + Offset(carLeft + carWidth * 0.44, cy + 18), + servicePlugRemoved ? const Color(0xFFFF2D55) : const Color(0xFFF59E0B), + ); + drawHudText('بطارية 384V DC', Offset(carLeft + carWidth * 0.46, cy - 38), const Color(0xFF10B981)); + drawHudText('منفذ الشحن', Offset(carLeft + carWidth * 0.76, carTop - 4), const Color(0xFF00F5D4)); + drawHudText('12V', Offset(carLeft + carWidth * 0.15, carBottom - 32), const Color(0xFF38BDF8)); + + // Front and Rear car orientation badges + drawHudText('مقدمة السيارة (Front)', Offset(carLeft + 4, carTop - 18), Colors.white38); + drawHudText('مؤخرة السيارة (Rear)', Offset(carRight - 90, carTop - 18), Colors.white38); } @override - bool shouldRepaint(covariant _EvDiagnosticsPainter oldDelegate) => true; + bool shouldRepaint(covariant _CarChassisPainter oldDelegate) => true; } + +// ============================================================================= +// ISOLATED COMPONENT BLUEPRINT ANATOMY PAINTER +// ============================================================================= +class _ComponentAnatomyPainter extends CustomPainter { + final String componentId; + final Color primaryColor; + final bool servicePlugRemoved; + final bool probesPlaced; + final String meterReading; + final String meterUnit; + final double pulseValue; + + _ComponentAnatomyPainter({ + required this.componentId, + required this.primaryColor, + required this.servicePlugRemoved, + required this.probesPlaced, + required this.meterReading, + required this.meterUnit, + required this.pulseValue, + }); + + @override + void paint(Canvas canvas, Size size) { + final w = size.width; + final h = size.height; + final cx = w / 2; + final cy = h / 2; + + // Technical Workshop Grid + final gridPaint = Paint() + ..color = Colors.white.withAlpha(7) + ..strokeWidth = 1.0; + for (double x = 0; x < w; x += 18) { + canvas.drawLine(Offset(x, 0), Offset(x, h), gridPaint); + } + for (double y = 0; y < h; y += 18) { + canvas.drawLine(Offset(0, y), Offset(w, y), gridPaint); + } + + switch (componentId) { + case 'battery': + _paintBatteryAnatomy(canvas, size, cx, cy); + break; + case 'inverter_motor': + _paintInverterMotorAnatomy(canvas, size, cx, cy); + break; + case 'service_plug': + _paintServicePlugAnatomy(canvas, size, cx, cy); + break; + case 'cables': + _paintCablesAnatomy(canvas, size, cx, cy); + break; + case 'charging_port': + _paintChargingPortAnatomy(canvas, size, cx, cy); + break; + case 'aux_battery': + _paintAuxBatteryAnatomy(canvas, size, cx, cy); + break; + default: + _paintBatteryAnatomy(canvas, size, cx, cy); + } + } + + void _drawLabel(Canvas canvas, String text, Offset pos, Color color, {double fontSize = 9.5, bool isLtr = false}) { + final tp = TextPainter( + text: TextSpan( + text: text, + style: TextStyle(color: color, fontSize: fontSize, fontWeight: FontWeight.bold), + ), + textDirection: isLtr ? TextDirection.ltr : TextDirection.rtl, + )..layout(); + tp.paint(canvas, pos); + } + + void _drawProbe(Canvas canvas, Offset targetPos, Color probeColor, String label, bool isTop) { + if (!probesPlaced) return; + + final wirePaint = Paint() + ..color = probeColor.withAlpha(190) + ..strokeWidth = 2.5 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + + final startX = isTop ? targetPos.dx - 40 : targetPos.dx + 40; + final startY = isTop ? 12.0 : 218.0; + + final wirePath = Path() + ..moveTo(startX, startY) + ..quadraticBezierTo( + isTop ? targetPos.dx - 20 : targetPos.dx + 20, + isTop ? targetPos.dy - 35 : targetPos.dy + 35, + targetPos.dx, + targetPos.dy, + ); + canvas.drawPath(wirePath, wirePaint); + + // Metallic Brass Probe Tip + final brassPaint = Paint()..color = const Color(0xFFFFD166); + canvas.drawCircle(targetPos, 4.5, brassPaint); + + // Glowing Probe Handle Ring + final ringPaint = Paint() + ..color = probeColor + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0; + canvas.drawCircle(targetPos, 8.0, ringPaint); + + // Floating Probe Callout Pill + final pillRect = RRect.fromRectAndRadius( + Rect.fromCenter( + center: Offset(startX, isTop ? startY + 12 : startY - 12), + width: 82, + height: 18, + ), + const Radius.circular(6), + ); + canvas.drawRRect(pillRect, Paint()..color = Colors.black.withAlpha(220)); + canvas.drawRRect(pillRect, Paint()..color = probeColor.withAlpha(120)..style = PaintingStyle.stroke..strokeWidth = 1.0); + + _drawLabel(canvas, label, Offset(startX - 36, isTop ? startY + 6 : startY - 18), probeColor, fontSize: 8.5); + } + + // =========================================================================== + // 1. HIGH VOLTAGE TRACTION BATTERY ANATOMY + // =========================================================================== + void _paintBatteryAnatomy(Canvas canvas, Size size, double cx, double cy) { + final w = size.width; + + // Outer Heavy Aluminum Casing + final caseRect = RRect.fromRectAndRadius( + Rect.fromCenter(center: Offset(cx, cy - 6), width: w * 0.88, height: 160), + const Radius.circular(14), + ); + canvas.drawRRect(caseRect, Paint()..color = const Color(0xFF071B17)); + canvas.drawRRect( + caseRect, + Paint() + ..color = const Color(0xFF10B981).withAlpha(120) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5, + ); + + // 4 Lithium NMC Module Banks + final moduleWidth = (w * 0.88 - 56) / 4; + final moduleStartX = cx - (w * 0.88) / 2 + 12; + + for (int i = 0; i < 4; i++) { + final mx = moduleStartX + (i * (moduleWidth + 8)); + final modRect = RRect.fromRectAndRadius( + Rect.fromLTWH(mx, cy - 62, moduleWidth, 112), + const Radius.circular(8), + ); + canvas.drawRRect(modRect, Paint()..color = const Color(0xFF0C2922)); + canvas.drawRRect( + modRect, + Paint() + ..color = const Color(0xFF00F5D4).withAlpha(80) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.0, + ); + + // Cell Slices inside module + for (int c = 1; c <= 3; c++) { + final cxPos = mx + (c * moduleWidth / 4); + canvas.drawLine( + Offset(cxPos, cy - 56), + Offset(cxPos, cy + 44), + Paint()..color = const Color(0xFF10B981).withAlpha(40)..strokeWidth = 1.2, + ); + } + _drawLabel(canvas, 'وحدة ${i + 1}', Offset(mx + 8, cy - 54), const Color(0xFF00F5D4), fontSize: 8.5); + } + + // BMS Master Controller on top edge + final bmsRect = RRect.fromRectAndRadius( + Rect.fromLTWH(cx - 70, cy + 56, 140, 24), + const Radius.circular(6), + ); + canvas.drawRRect(bmsRect, Paint()..color = const Color(0xFF0F3229)); + canvas.drawRRect(bmsRect, Paint()..color = const Color(0xFF00F5D4)..style = PaintingStyle.stroke..strokeWidth = 1.0); + // BMS Status LED + canvas.drawCircle(Offset(cx - 56, cy + 68), 3.5, Paint()..color = Color.lerp(const Color(0xFF00F5D4), const Color(0xFF10B981), pulseValue)!); + _drawLabel(canvas, 'كمبيوتر البطارية BMS Controller', Offset(cx - 48, cy + 62), Colors.white, fontSize: 8.5); + + // MSD Center Socket + final msdSocket = Offset(cx, cy - 6); + final msdColor = servicePlugRemoved ? const Color(0xFFFF2D55) : const Color(0xFFF59E0B); + canvas.drawCircle(msdSocket, 12, Paint()..color = const Color(0xFF1F1608)); + canvas.drawCircle(msdSocket, 12, Paint()..color = msdColor..style = PaintingStyle.stroke..strokeWidth = 2.0); + canvas.drawCircle(msdSocket, 6, Paint()..color = msdColor); + _drawLabel(canvas, servicePlugRemoved ? 'MSD مفصول 🛑' : 'MSD مقفل ⚡', Offset(cx - 28, cy + 10), msdColor, fontSize: 8); + + // High Voltage Terminals (+ and -) + final posTerm = Offset(cx + (w * 0.88) / 2 - 16, cy - 35); + final negTerm = Offset(cx + (w * 0.88) / 2 - 16, cy + 22); + + canvas.drawCircle(posTerm, 8, Paint()..color = Colors.red.shade900); + canvas.drawCircle(posTerm, 5, Paint()..color = Colors.redAccent); + _drawLabel(canvas, '+HV', Offset(posTerm.dx - 8, posTerm.dy - 16), Colors.redAccent, fontSize: 9, isLtr: true); + + canvas.drawCircle(negTerm, 8, Paint()..color = Colors.black); + canvas.drawCircle(negTerm, 5, Paint()..color = Colors.grey.shade600); + _drawLabel(canvas, '-HV', Offset(negTerm.dx - 8, negTerm.dy + 7), Colors.white70, fontSize: 9, isLtr: true); + + // Probes Placement + _drawProbe(canvas, posTerm, Colors.redAccent, 'مجس أحمر (+)', true); + _drawProbe(canvas, negTerm, Colors.white70, 'مجس أسود (-)', false); + + // Readout Badge + final readoutRect = RRect.fromRectAndRadius( + Rect.fromCenter(center: Offset(cx, cy - 72), width: 150, height: 22), + const Radius.circular(6), + ); + canvas.drawRRect(readoutRect, Paint()..color = Colors.black87); + canvas.drawRRect(readoutRect, Paint()..color = const Color(0xFF00F5D4)..style = PaintingStyle.stroke..strokeWidth = 1.0); + _drawLabel( + canvas, + 'الجهد: ${servicePlugRemoved ? "0.00 V (معزول)" : "$meterReading $meterUnit"}', + Offset(cx - 68, cy - 78), + servicePlugRemoved ? const Color(0xFF10B981) : const Color(0xFF00F5D4), + fontSize: 10, + ); + } + + // =========================================================================== + // 2. INVERTER & TRACTION MOTOR ANATOMY + // =========================================================================== + void _paintInverterMotorAnatomy(Canvas canvas, Size size, double cx, double cy) { + final w = size.width; + + // Left: Inverter Box (Power Electronics) + final invRect = RRect.fromRectAndRadius( + Rect.fromCenter(center: Offset(cx - w * 0.22, cy), width: w * 0.42, height: 160), + const Radius.circular(12), + ); + canvas.drawRRect(invRect, Paint()..color = const Color(0xFF16102A)); + canvas.drawRRect(invRect, Paint()..color = const Color(0xFF8B5CF6)..style = PaintingStyle.stroke..strokeWidth = 1.5); + _drawLabel(canvas, 'محول القدرة (Inverter)', Offset(cx - w * 0.22 - 50, cy - 72), const Color(0xFF8B5CF6), fontSize: 9.5); + + // 2 DC Filter Capacitors inside inverter + final cap1 = Offset(cx - w * 0.32, cy - 25); + final cap2 = Offset(cx - w * 0.32, cy + 25); + canvas.drawCircle(cap1, 14, Paint()..color = const Color(0xFF261D42)); + canvas.drawCircle(cap1, 14, Paint()..color = Colors.white30..style = PaintingStyle.stroke..strokeWidth = 1.0); + canvas.drawCircle(cap2, 14, Paint()..color = const Color(0xFF261D42)); + canvas.drawCircle(cap2, 14, Paint()..color = Colors.white30..style = PaintingStyle.stroke..strokeWidth = 1.0); + _drawLabel(canvas, 'مكثف DC', Offset(cap1.dx - 18, cap1.dy - 4), Colors.white60, fontSize: 8); + + // 3 IGBT Switch Pairs (U, V, W) + for (int s = 0; s < 3; s++) { + final sy = cy - 36 + (s * 36); + final igbtRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset(cx - w * 0.14, sy), width: 34, height: 26), const Radius.circular(4)); + canvas.drawRRect(igbtRect, Paint()..color = const Color(0xFF382963)); + canvas.drawRRect(igbtRect, Paint()..color = const Color(0xFF00F5D4)..style = PaintingStyle.stroke..strokeWidth = 1.0); + _drawLabel(canvas, s == 0 ? 'IGBT U' : (s == 1 ? 'IGBT V' : 'IGBT W'), Offset(cx - w * 0.14 - 14, sy - 5), Colors.white, fontSize: 7.5, isLtr: true); + } + + // Right: 3-Phase Electric Motor + final motorCenter = Offset(cx + w * 0.24, cy); + canvas.drawCircle(motorCenter, 68, Paint()..color = const Color(0xFF0F1E28)); + canvas.drawCircle(motorCenter, 68, Paint()..color = const Color(0xFF00F5D4)..style = PaintingStyle.stroke..strokeWidth = 1.5); + + // Stator Coils (U: Red, V: Green, W: Blue) + final coilColors = [const Color(0xFFFF2D55), const Color(0xFF10B981), const Color(0xFF38BDF8)]; + for (int i = 0; i < 6; i++) { + final angle = (i * 60) * (3.14159 / 180); + final coilPos = Offset(motorCenter.dx + 48 * math.cos(angle), motorCenter.dy + 48 * math.sin(angle)); + canvas.drawCircle(coilPos, 9, Paint()..color = coilColors[i % 3].withAlpha(60)); + canvas.drawCircle(coilPos, 9, Paint()..color = coilColors[i % 3]..style = PaintingStyle.stroke..strokeWidth = 1.5); + } + + // Permanent Magnet Rotor in Center + canvas.drawCircle(motorCenter, 28, Paint()..color = const Color(0xFF1E293B)); + canvas.drawCircle(motorCenter, 28, Paint()..color = Colors.amber..style = PaintingStyle.stroke..strokeWidth = 2.0); + _drawLabel(canvas, 'عضو دوار N/S', Offset(motorCenter.dx - 22, motorCenter.dy - 5), Colors.white, fontSize: 8); + + // Connecting 3-Phase Cables from Inverter to Motor + final cablePaint = Paint()..strokeWidth = 2.5..style = PaintingStyle.stroke; + canvas.drawLine(Offset(cx - w * 0.08, cy - 28), Offset(motorCenter.dx - 56, cy - 28), cablePaint..color = coilColors[0]); + canvas.drawLine(Offset(cx - w * 0.08, cy), Offset(motorCenter.dx - 62, cy), cablePaint..color = coilColors[1]); + canvas.drawLine(Offset(cx - w * 0.08, cy + 28), Offset(motorCenter.dx - 56, cy + 28), cablePaint..color = coilColors[2]); + + // Probes Placement on Motor Phase Terminals + final probeUPos = Offset(motorCenter.dx - 56, cy - 28); + final probeVPos = Offset(motorCenter.dx - 62, cy); + _drawProbe(canvas, probeUPos, Colors.redAccent, 'مجس U (طور 1)', true); + _drawProbe(canvas, probeVPos, Colors.black87, 'مجس V (طور 2)', false); + + // Readout Badge + final readoutRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset(cx, cy - 72), width: 140, height: 22), const Radius.circular(6)); + canvas.drawRRect(readoutRect, Paint()..color = Colors.black87); + canvas.drawRRect(readoutRect, Paint()..color = const Color(0xFF8B5CF6)..style = PaintingStyle.stroke..strokeWidth = 1.0); + _drawLabel(canvas, 'مقاومة الأطوار: $meterReading $meterUnit', Offset(cx - 62, cy - 78), const Color(0xFF00F5D4), fontSize: 9.5); + } + + // =========================================================================== + // 3. MANUAL SERVICE DISCONNECT (MSD) ANATOMY + // =========================================================================== + void _paintServicePlugAnatomy(Canvas canvas, Size size, double cx, double cy) { + // MSD Main Lever Handle + final handleColor = servicePlugRemoved ? const Color(0xFFFF2D55) : const Color(0xFFF59E0B); + final leverRect = RRect.fromRectAndRadius( + Rect.fromCenter(center: Offset(cx, cy - 40), width: 120, height: 28), + const Radius.circular(8), + ); + canvas.drawRRect(leverRect, Paint()..color = handleColor.withAlpha(40)); + canvas.drawRRect(leverRect, Paint()..color = handleColor..style = PaintingStyle.stroke..strokeWidth = 2.0); + _drawLabel(canvas, servicePlugRemoved ? 'مقبض الأمان (مرفوع ومفصول)' : 'مقبض الأمان (مغلق ومثبت)', Offset(cx - 52, cy - 46), Colors.white, fontSize: 9); + + // High Voltage Ceramic Fuse in Center (400A / 450V) + final fuseRect = RRect.fromRectAndRadius( + Rect.fromCenter(center: Offset(cx, cy + 12), width: 130, height: 46), + const Radius.circular(10), + ); + canvas.drawRRect(fuseRect, Paint()..color = const Color(0xFF1E1910)); + canvas.drawRRect(fuseRect, Paint()..color = Colors.white54..style = PaintingStyle.stroke..strokeWidth = 1.5); + + // Fuse Metallic End Caps + final capLeft = RRect.fromRectAndRadius(Rect.fromLTWH(cx - 65, cy - 11, 20, 46), const Radius.circular(6)); + final capRight = RRect.fromRectAndRadius(Rect.fromLTWH(cx + 45, cy - 11, 20, 46), const Radius.circular(6)); + canvas.drawRRect(capLeft, Paint()..color = const Color(0xFFD1D5DB)); + canvas.drawRRect(capRight, Paint()..color = const Color(0xFFD1D5DB)); + _drawLabel(canvas, 'فيوز سيراميك 400A / 450V', Offset(cx - 42, cy + 6), const Color(0xFFFFD166), fontSize: 8.5); + + // Main High Voltage Copper Blade Terminals (Long Blades) + final blade1 = RRect.fromRectAndRadius(Rect.fromLTWH(cx - 48, cy + 58, 14, 34), const Radius.circular(3)); + final blade2 = RRect.fromRectAndRadius(Rect.fromLTWH(cx + 34, cy + 58, 14, 34), const Radius.circular(3)); + final copperPaint = Paint()..color = const Color(0xFFF97316); + canvas.drawRRect(blade1, copperPaint); + canvas.drawRRect(blade2, copperPaint); + _drawLabel(canvas, 'شفرات الجهد العالي الرئيسية', Offset(cx - 52, cy + 76), Colors.white70, fontSize: 8); + + // HVIL Interlock Miniature Pins (Deliberately drawn SHORTER) + final hvil1 = RRect.fromRectAndRadius(Rect.fromLTWH(cx - 18, cy + 58, 6, 18), const Radius.circular(2)); + final hvil2 = RRect.fromRectAndRadius(Rect.fromLTWH(cx + 12, cy + 58, 6, 18), const Radius.circular(2)); + final hvilPaint = Paint()..color = const Color(0xFF00F5D4); + canvas.drawRRect(hvil1, hvilPaint); + canvas.drawRRect(hvil2, hvilPaint); + _drawLabel(canvas, 'دبابيس HVIL (أقصر لتفصل أولاً)', Offset(cx - 56, cy + 60), const Color(0xFF00F5D4), fontSize: 7.5); + + // Probes Placement across Fuse End Caps + _drawProbe(canvas, Offset(cx - 55, cy + 12), Colors.redAccent, 'طرف 1 للفيوز', true); + _drawProbe(canvas, Offset(cx + 55, cy + 12), Colors.white70, 'طرف 2 للفيوز', false); + + // Readout Badge + final readoutRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset(cx, cy - 74), width: 160, height: 22), const Radius.circular(6)); + canvas.drawRRect(readoutRect, Paint()..color = Colors.black87); + canvas.drawRRect(readoutRect, Paint()..color = handleColor..style = PaintingStyle.stroke..strokeWidth = 1.0); + _drawLabel(canvas, 'فحص الفيوز: $meterReading $meterUnit', Offset(cx - 72, cy - 80), const Color(0xFF10B981), fontSize: 9.5); + } + + // =========================================================================== + // 4. HIGH VOLTAGE ORANGE CABLES ANATOMY + // =========================================================================== + void _paintCablesAnatomy(Canvas canvas, Size size, double cx, double cy) { + // Layered Concentric Cross-Section of High-Voltage Cable + final cableCenter = Offset(cx - 55, cy); + + // Layer 4: High-Visibility Orange Outer Jacket (RAL 2003) + canvas.drawCircle(cableCenter, 68, Paint()..color = const Color(0xFFFF7A00)); + canvas.drawCircle(cableCenter, 68, Paint()..color = Colors.white24..style = PaintingStyle.stroke..strokeWidth = 2.0); + + // Layer 3: Braided Aluminum EMC Shielding Mesh + canvas.drawCircle(cableCenter, 52, Paint()..color = const Color(0xFF475569)); + canvas.drawCircle(cableCenter, 52, Paint()..color = const Color(0xFF94A3B8)..style = PaintingStyle.stroke..strokeWidth = 2.0); + + // Layer 2: XLPE High-Dielectric Silicone Insulation + canvas.drawCircle(cableCenter, 38, Paint()..color = const Color(0xFFE2E8F0)); + canvas.drawCircle(cableCenter, 38, Paint()..color = Colors.white..style = PaintingStyle.stroke..strokeWidth = 1.5); + + // Layer 1: Stranded Pure Copper Core + canvas.drawCircle(cableCenter, 22, Paint()..color = const Color(0xFFEA580C)); + canvas.drawCircle(cableCenter, 22, Paint()..color = const Color(0xFFFDBA74)..style = PaintingStyle.stroke..strokeWidth = 2.0); + + // Chassis Ground Terminal Plate on Right + final gndPlate = Offset(cx + 80, cy); + final gndRect = RRect.fromRectAndRadius(Rect.fromCenter(center: gndPlate, width: 65, height: 48), const Radius.circular(8)); + canvas.drawRRect(gndRect, Paint()..color = const Color(0xFF1E293B)); + canvas.drawRRect(gndRect, Paint()..color = Colors.white38..style = PaintingStyle.stroke..strokeWidth = 1.5); + _drawLabel(canvas, 'شاسيه السيارة', Offset(gndPlate.dx - 26, gndPlate.dy - 12), Colors.white, fontSize: 8.5); + _drawLabel(canvas, '(Chassis GND)', Offset(gndPlate.dx - 30, gndPlate.dy + 4), const Color(0xFF00F5D4), fontSize: 8, isLtr: true); + + // Callout Lines for Layers + _drawLabel(canvas, '4. غلاف برتقالي خارجي', Offset(cx + 5, cy - 70), const Color(0xFFFF7A00), fontSize: 8.5); + _drawLabel(canvas, '3. شبكة حجب EMC', Offset(cx + 5, cy - 54), const Color(0xFF94A3B8), fontSize: 8.5); + _drawLabel(canvas, '2. عازل XLPE سيليكوني', Offset(cx + 5, cy - 38), Colors.white, fontSize: 8.5); + _drawLabel(canvas, '1. نحاس مجدول (250A)', Offset(cx + 5, cy - 22), const Color(0xFFEA580C), fontSize: 8.5); + + // Probes: Red on Copper Core, Black on Chassis Ground + _drawProbe(canvas, cableCenter, Colors.redAccent, 'قلب النحاس', true); + _drawProbe(canvas, gndPlate, Colors.white70, 'شاسيه السيارة', false); + + // Readout Badge + final readoutRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset(cx, cy + 74), width: 170, height: 22), const Radius.circular(6)); + canvas.drawRRect(readoutRect, Paint()..color = Colors.black87); + canvas.drawRRect(readoutRect, Paint()..color = const Color(0xFFFF7A00)..style = PaintingStyle.stroke..strokeWidth = 1.0); + _drawLabel(canvas, 'مقاومة العزل: $meterReading $meterUnit', Offset(cx - 76, cy + 68), const Color(0xFF10B981), fontSize: 9.5); + } + + // =========================================================================== + // 5. COMBINED CHARGING PORT (CCS2) ANATOMY + // =========================================================================== + void _paintChargingPortAnatomy(Canvas canvas, Size size, double cx, double cy) { + // Upper Type 2 Connector Housing + final upperCenter = Offset(cx, cy - 26); + canvas.drawCircle(upperCenter, 48, Paint()..color = const Color(0xFF0A1F22)); + canvas.drawCircle(upperCenter, 48, Paint()..color = const Color(0xFF00F5D4)..style = PaintingStyle.stroke..strokeWidth = 1.5); + + // Upper Pins: PE (Top Center, Large) + final pePos = Offset(upperCenter.dx, upperCenter.dy - 24); + canvas.drawCircle(pePos, 7, Paint()..color = const Color(0xFF10B981)); + _drawLabel(canvas, 'PE', Offset(pePos.dx - 6, pePos.dy - 16), const Color(0xFF10B981), fontSize: 8, isLtr: true); + + // Middle Pins: L1, L2, L3, N + final pinOffsets = [ + Offset(upperCenter.dx - 22, upperCenter.dy - 6), + Offset(upperCenter.dx - 8, upperCenter.dy - 4), + Offset(upperCenter.dx + 8, upperCenter.dy - 4), + Offset(upperCenter.dx + 22, upperCenter.dy - 6), + ]; + for (final p in pinOffsets) { + canvas.drawCircle(p, 5, Paint()..color = const Color(0xFFFFD166)); + } + + // Lower Type 2 Pins: CP & PP + final cpPos = Offset(upperCenter.dx - 14, upperCenter.dy + 18); + final ppPos = Offset(upperCenter.dx + 14, upperCenter.dy + 18); + canvas.drawCircle(cpPos, 4, Paint()..color = const Color(0xFF38BDF8)); + canvas.drawCircle(ppPos, 4, Paint()..color = const Color(0xFFF59E0B)); + _drawLabel(canvas, 'CP', Offset(cpPos.dx - 6, cpPos.dy + 6), const Color(0xFF38BDF8), fontSize: 7.5, isLtr: true); + _drawLabel(canvas, 'PP', Offset(ppPos.dx - 6, ppPos.dy + 6), const Color(0xFFF59E0B), fontSize: 7.5, isLtr: true); + + // Lower DC Fast Charging Housing + final dcRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset(cx, cy + 48), width: 90, height: 46), const Radius.circular(12)); + canvas.drawRRect(dcRect, Paint()..color = const Color(0xFF08181A)); + canvas.drawRRect(dcRect, Paint()..color = const Color(0xFF00F5D4)..style = PaintingStyle.stroke..strokeWidth = 1.5); + + // Two Large DC Pins (+ and -) + final dcPlus = Offset(cx - 24, cy + 48); + final dcMinus = Offset(cx + 24, cy + 48); + canvas.drawCircle(dcPlus, 10, Paint()..color = Colors.red.shade800); + canvas.drawCircle(dcMinus, 10, Paint()..color = Colors.black); + _drawLabel(canvas, 'DC+', Offset(dcPlus.dx - 9, dcPlus.dy - 5), Colors.white, fontSize: 8, isLtr: true); + _drawLabel(canvas, 'DC-', Offset(dcMinus.dx - 8, dcMinus.dy - 5), Colors.white, fontSize: 8, isLtr: true); + + // Probes on PP and PE + _drawProbe(canvas, ppPos, Colors.redAccent, 'دبوس PP', true); + _drawProbe(canvas, pePos, Colors.black87, 'دبوس PE', false); + + // Readout Badge + final readoutRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset(cx, cy - 76), width: 150, height: 22), const Radius.circular(6)); + canvas.drawRRect(readoutRect, Paint()..color = Colors.black87); + canvas.drawRRect(readoutRect, Paint()..color = const Color(0xFF00F5D4)..style = PaintingStyle.stroke..strokeWidth = 1.0); + _drawLabel(canvas, 'إشارة القرب PP: $meterReading $meterUnit', Offset(cx - 68, cy - 82), const Color(0xFF00F5D4), fontSize: 9.5); + } + + // =========================================================================== + // 6. 12V AUXILIARY BATTERY & DC-DC CONVERTER ANATOMY + // =========================================================================== + void _paintAuxBatteryAnatomy(Canvas canvas, Size size, double cx, double cy) { + final w = size.width; + + // Left: 12V Auxiliary Battery Case + final battRect = RRect.fromRectAndRadius( + Rect.fromCenter(center: Offset(cx - w * 0.22, cy), width: w * 0.38, height: 130), + const Radius.circular(10), + ); + canvas.drawRRect(battRect, Paint()..color = const Color(0xFF0C2436)); + canvas.drawRRect(battRect, Paint()..color = const Color(0xFF38BDF8)..style = PaintingStyle.stroke..strokeWidth = 1.5); + _drawLabel(canvas, 'بطارية 12V المساعدة', Offset(cx - w * 0.22 - 42, cy - 54), const Color(0xFF38BDF8), fontSize: 9); + + // Positive and Negative Posts + final posPost = Offset(cx - w * 0.30, cy - 25); + final negPost = Offset(cx - w * 0.14, cy - 25); + canvas.drawCircle(posPost, 8, Paint()..color = Colors.redAccent); + canvas.drawCircle(negPost, 8, Paint()..color = Colors.black); + _drawLabel(canvas, '+12V', Offset(posPost.dx - 10, posPost.dy - 16), Colors.redAccent, fontSize: 8.5, isLtr: true); + _drawLabel(canvas, '-GND', Offset(negPost.dx - 10, negPost.dy - 16), Colors.white70, fontSize: 8.5, isLtr: true); + + // Right: DC-DC Stepdown Converter (384V -> 14V) + final dcRect = RRect.fromRectAndRadius( + Rect.fromCenter(center: Offset(cx + w * 0.22, cy), width: w * 0.38, height: 130), + const Radius.circular(10), + ); + canvas.drawRRect(dcRect, Paint()..color = const Color(0xFF132D28)); + canvas.drawRRect(dcRect, Paint()..color = const Color(0xFF10B981)..style = PaintingStyle.stroke..strokeWidth = 1.5); + _drawLabel(canvas, 'محول خفض الجهد DC-DC', Offset(cx + w * 0.22 - 50, cy - 54), const Color(0xFF10B981), fontSize: 9); + + // Internal High-Frequency Isolation Transformer + final transCenter = Offset(cx + w * 0.22, cy); + canvas.drawCircle(transCenter, 22, Paint()..color = const Color(0xFF1F443C)); + canvas.drawCircle(transCenter, 22, Paint()..color = const Color(0xFF00F5D4)..style = PaintingStyle.stroke..strokeWidth = 1.5); + _drawLabel(canvas, '384V -> 14V', Offset(transCenter.dx - 22, transCenter.dy - 5), Colors.white, fontSize: 7.5, isLtr: true); + + // Charging Link from DC-DC to 12V Battery + final linkPaint = Paint()..color = const Color(0xFF00F5D4)..strokeWidth = 2.0..style = PaintingStyle.stroke; + canvas.drawLine(Offset(cx + w * 0.03, cy + 15), Offset(cx - w * 0.03, cy + 15), linkPaint); + _drawLabel(canvas, 'تيار شحن 14.2V', Offset(cx - 28, cy + 22), const Color(0xFF00F5D4), fontSize: 8); + + // Probes across 12V Battery Posts + _drawProbe(canvas, posPost, Colors.redAccent, '+12V', true); + _drawProbe(canvas, negPost, Colors.white70, '-GND', false); + + // Readout Badge + final readoutRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset(cx, cy - 74), width: 140, height: 22), const Radius.circular(6)); + canvas.drawRRect(readoutRect, Paint()..color = Colors.black87); + canvas.drawRRect(readoutRect, Paint()..color = const Color(0xFF38BDF8)..style = PaintingStyle.stroke..strokeWidth = 1.0); + _drawLabel(canvas, 'جهد الشحن: $meterReading $meterUnit', Offset(cx - 62, cy - 80), const Color(0xFF00F5D4), fontSize: 9.5); + } + + @override + bool shouldRepaint(covariant _ComponentAnatomyPainter oldDelegate) => true; +} + diff --git a/apps/student_app/pubspec.lock b/apps/student_app/pubspec.lock index 5b3adf5..c306ddb 100644 --- a/apps/student_app/pubspec.lock +++ b/apps/student_app/pubspec.lock @@ -340,10 +340,10 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -356,10 +356,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" nested: dependency: transitive description: @@ -585,10 +585,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.11" typed_data: dependency: transitive description: diff --git a/apps/student_app/test/guardian_and_error_notebook_integration_test.dart b/apps/student_app/test/guardian_and_error_notebook_integration_test.dart new file mode 100644 index 0000000..9b89f35 --- /dev/null +++ b/apps/student_app/test/guardian_and_error_notebook_integration_test.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:student_app/data/models/lesson_model.dart'; +import 'package:student_app/presentation/screens/curriculum/lesson_homework_sheet.dart'; +import 'package:student_app/presentation/screens/notebook/smart_error_notebook_screen.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Guardian Child Model & Error Metrics Integration', () { + test('correctly parses server-authoritative error notebook metrics', () { + final json = { + 'student': { + 'id': 42, + 'uuid': 'student-uuid-42', + 'full_name': 'عمر الخطيب', + 'national_id': '****1234', + 'grade_level': 'الصف العاشر الأساسي', + 'stream': 'علمي', + 'school_name': 'مدرسة الملك عبد الله للتميز', + }, + 'metrics': { + 'readiness_score': 88.5, + 'exams_passed_count': 7, + 'exams_total_count': 8, + 'error_notebook': { + 'total_errors': 6, + 'mastered_count': 4, + 'pending_count': 2, + 'mastery_percentage': 66.7, + }, + }, + }; + + final model = GuardianChildModel.fromJson(json); + + expect(model.id, 42); + expect(model.name, 'عمر الخطيب'); + expect(model.readinessScore, 88.5); + expect(model.examsPassed, 7); + expect(model.examsTotal, 8); + expect(model.errorTotalCount, 6); + expect(model.errorMasteredCount, 4); + expect(model.errorPendingCount, 2); + expect(model.errorMasteryRate, 66.7); + }); + + test('handles empty or perfect error metrics gracefully', () { + final json = { + 'student': {'id': 1, 'full_name': 'سارة'}, + 'metrics': {'readiness_score': 95.0}, + }; + + final model = GuardianChildModel.fromJson(json); + + expect(model.errorTotalCount, 0); + expect(model.errorMasteredCount, 0); + expect(model.errorPendingCount, 0); + expect(model.errorMasteryRate, 100.0); + }); + }); + + group('Smart Error Notebook Guardian Mode UI Tests', () { + testWidgets('renders guardian inspection mode with child name and lock badge', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: SmartErrorNotebookScreen( + isGuardianMode: true, + studentName: 'عمر الخطيب', + studentId: 42, + ), + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + expect(find.textContaining('عمر الخطيب'), findsOneWidget); + expect(find.text('رقابة الأهل 👨‍👧‍👦'), findsOneWidget); + }); + + testWidgets('renders student self-study mode without guardian badge', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: SmartErrorNotebookScreen( + isGuardianMode: false, + ), + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + expect(find.text('دفتر الأخطاء الذكي والمسار العلاجي'), findsOneWidget); + expect(find.text('رقابة الأهل 👨‍👧‍👦'), findsNothing); + }); + }); + + group('Homework Sheet Error Logging', () { + testWidgets('LessonHomeworkSheet mounts and handles answer verification', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: LessonHomeworkSheet( + subjectId: 'physics_10', + subjectTitle: 'الفيزياء', + lessonId: 'lesson_01', + lessonTitle: 'الكميات القياسية والمتجهة', + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.textContaining('واجب الدرس'), findsOneWidget); + expect(find.textContaining('التمرين 1'), findsOneWidget); + expect(find.text('تحقق من الإجابة 🚀'), findsOneWidget); + }); + }); +} diff --git a/apps/student_app/test/scaffolded_learning_and_teacher_selection_test.dart b/apps/student_app/test/scaffolded_learning_and_teacher_selection_test.dart new file mode 100644 index 0000000..794e3dc --- /dev/null +++ b/apps/student_app/test/scaffolded_learning_and_teacher_selection_test.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:student_app/data/models/subject_model.dart'; +import 'package:student_app/data/repositories/curriculum_question_bank.dart'; +import 'package:student_app/data/repositories/curriculum_repository.dart'; +import 'package:student_app/presentation/screens/curriculum/teacher_selection_sheet.dart'; +import 'package:student_app/presentation/screens/player/scaffolded_thinking_pause_sheet.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CurriculumQuestionBank Textbooks Isolation', () { + test('Islamic textbooks return authentic Grade 10 books', () { + final books = CurriculumQuestionBank.getSubjectTextbooks( + subjectId: 'islamic_10', + subjectTitle: 'التربية الإسلامية', + ); + expect(books.length, 2); + expect(books.first.title, contains('التربية الإسلامية')); + expect(books.first.assetType, 'textbook'); + expect(books.first.mimeType, 'application/pdf'); + }); + + test('Math textbooks include exercise book', () { + final books = CurriculumQuestionBank.getSubjectTextbooks( + subjectId: 'math_10', + subjectTitle: 'الرياضيات', + ); + expect(books.length, 3); + expect(books.any((b) => b.title.contains('التمارين')), isTrue); + }); + + test('Physics textbooks return authentic semesters', () { + final books = CurriculumQuestionBank.getSubjectTextbooks( + subjectId: 'physics_10', + subjectTitle: 'الفيزياء', + ); + expect(books.length, 2); + expect(books.any((b) => b.title.contains('الفيزياء')), isTrue); + }); + }); + + group('TeacherSelectionSheet UI Tests', () { + testWidgets('renders multiple teachers sorted by rating and allows choice', (tester) async { + const lesson = CurriculumLessonItemModel( + id: 'lesson_math_01', + title: 'حل نظام مكون من معادلتين تربيعية وخطية', + durationSeconds: 1200, + hasVideo: true, + checkpointsCount: 3, + outcomes: ['عزل المتغير', 'التعويض'], + ); + + final videos = [ + const PublishedLessonVideoModel( + videoVersionId: 'v_ahmad', + submissionId: 'sub_1', + teacherName: 'أ. أحمد الطراونة', + rating: 4.8, + ratingCount: 95, + isNew: false, + ), + const PublishedLessonVideoModel( + videoVersionId: 'v_khalid', + submissionId: 'sub_2', + teacherName: 'أ. خالد الحنيطي', + rating: 4.9, + ratingCount: 140, + isNew: false, + ), + ]; + + PublishedLessonVideoModel? selected; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () async { + selected = await TeacherSelectionSheet.show( + context, + lesson: lesson, + videos: videos, + ); + }, + child: const Text('افتح اختيار المعلم'), + ), + ), + ), + ), + ); + + // Open sheet + await tester.tap(find.text('افتح اختيار المعلم')); + await tester.pumpAndSettle(); + + // Top rated teacher (Khalid, 4.9) should appear first with badge + expect(find.text('اختر شرح المعلم المعتمد 👨‍🏫'), findsOneWidget); + expect(find.text('الأعلى تقييماً'), findsOneWidget); + expect(find.text('أ. خالد الحنيطي'), findsOneWidget); + expect(find.text('أ. أحمد الطراونة'), findsOneWidget); + + // Tap on Khalid + await tester.tap(find.text('أ. خالد الحنيطي')); + await tester.pumpAndSettle(); + + // Verified selection + expect(selected, isNotNull); + expect(selected!.teacherName, 'أ. خالد الحنيطي'); + expect(selected!.videoVersionId, 'v_khalid'); + }); + }); + + group('ScaffoldedThinkingPauseSheet UI Tests', () { + testWidgets('enforces 60-second thinking phase and unmasks steps progressively', (tester) async { + const lesson = CurriculumLessonItemModel( + id: 'lesson_math_01', + title: 'حل نظام مكون من معادلتين', + durationSeconds: 900, + hasVideo: true, + checkpointsCount: 2, + outcomes: ['حل الأنظمة'], + ); + + tester.view.physicalSize = const Size(1080, 2200); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + + bool completed = false; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () { + ScaffoldedThinkingPauseSheet.show( + context, + lesson: lesson, + subject: const SubjectModel( + id: 'math_10', + title: 'الرياضيات', + englishTitle: 'Mathematics', + iconCode: 'function', + primaryColor: Colors.blue, + secondaryColor: Colors.cyan, + ), + onCompleted: () => completed = true, + ); + }, + child: const Text('افتح وقفة التفكير'), + ), + ), + ), + ), + ); + + // Open sheet + await tester.tap(find.text('افتح وقفة التفكير')); + await tester.pumpAndSettle(); + + // Check thinking phase elements + expect(find.text('وقفة تفكير وبناء الحل خطوة بخطوة ⏱️'), findsOneWidget); + expect(find.text('وقفة تأمل ذهني مستقل (60 ثانية) 🧠'), findsOneWidget); + expect(find.text('أنا جاهز، ابدأ بناء الحل خطوة بخطوة 🚀'), findsOneWidget); + + // Tap ready button to skip countdown and enter step-by-step unmasking + await tester.tap(find.text('أنا جاهز، ابدأ بناء الحل خطوة بخطوة 🚀')); + await tester.pumpAndSettle(); + + // Step 1 should be revealed + expect(find.text('بناء الحل التراكمي خطوة بخطوة 📐'), findsOneWidget); + expect(find.text('الخطوة 1 من 4'), findsOneWidget); + expect(find.text('الخطوة 1: عزل المتغير في المعادلة الخطية'), findsOneWidget); + + // Reveal Step 2 + expect(find.text('اكشف الخطوة التالية (2 من 4) ⬇️'), findsOneWidget); + await tester.tap(find.text('اكشف الخطوة التالية (2 من 4) ⬇️')); + await tester.pumpAndSettle(); + expect(find.text('الخطوة 2: التعويض في المعادلة التربيعية وتصفيرها'), findsOneWidget); + + // Reveal Step 3 + expect(find.text('اكشف الخطوة التالية (3 من 4) ⬇️'), findsOneWidget); + await tester.tap(find.text('اكشف الخطوة التالية (3 من 4) ⬇️')); + await tester.pumpAndSettle(); + expect(find.text('الخطوة 3: حساب المميز والتحليل إلى العوامل'), findsOneWidget); + + // Reveal Step 4 + expect(find.text('اكشف الخطوة التالية (4 من 4) ⬇️'), findsOneWidget); + await tester.tap(find.text('اكشف الخطوة التالية (4 من 4) ⬇️')); + await tester.pumpAndSettle(); + expect(find.text('الخطوة 4: إيجاد الأزواج المرتبة والتحقق البياني'), findsOneWidget); + + // Completion button + expect(find.text('اكتمل بناء الحل المنهجي بنجاح! استمر 🎯'), findsOneWidget); + expect(completed, isTrue); + }); + }); +} diff --git a/apps/student_app/test/subject_hub_decoupling_test.dart b/apps/student_app/test/subject_hub_decoupling_test.dart new file mode 100644 index 0000000..1ee0439 --- /dev/null +++ b/apps/student_app/test/subject_hub_decoupling_test.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:student_app/data/repositories/curriculum_question_bank.dart'; +import 'package:student_app/presentation/screens/curriculum/lesson_homework_sheet.dart'; +import 'package:student_app/presentation/screens/exams/adaptive_exam_screen.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CurriculumQuestionBank Subject Isolation & Decoupling', () { + test('Islamic Studies question bank contains ZERO math equations', () { + final exam = CurriculumQuestionBank.getUnitExam( + subjectId: 'islamic_10', + unitKey: 'unit_01', + ); + + expect(exam.title, contains('القرآن الكريم')); + expect(exam.questions.isNotEmpty, isTrue); + + for (final q in exam.questions) { + // Assert zero math contamination + expect(q.questionText.contains('x²'), isFalse, reason: 'Math leaked into Islamic exam: ${q.questionText}'); + expect(q.questionText.contains('معادلة'), isFalse, reason: 'Math leaked into Islamic exam: ${q.questionText}'); + expect(q.questionText.contains('مشتق'), isFalse, reason: 'Calculus leaked into Islamic exam: ${q.questionText}'); + // Assert Islamic topic relevance + expect( + q.topicTag.contains('قرآن') || + q.topicTag.contains('بيع') || + q.topicTag.contains('معاملات') || + q.topicTag.contains('فقه') || + q.topicTag.contains('سيرة') || + q.topicTag.contains('إسلام') || + q.topicTag.contains('حديث') || + q.topicTag.contains('آداب'), + isTrue, + reason: 'Non-Islamic topic tag in Islamic exam: ${q.topicTag}', + ); + } + }); + + test('Physics question bank contains authentic physics concepts', () { + final examU1 = CurriculumQuestionBank.getUnitExam( + subjectId: 'physics_10', + unitKey: 'unit_01', + ); + expect(examU1.title, contains('المتجهات')); + expect(examU1.questions.any((q) => q.questionText.contains('متجه')), isTrue); + + final examU2 = CurriculumQuestionBank.getUnitExam( + subjectId: 'physics_10', + unitKey: 'unit_02', + ); + expect(examU2.title, contains('المقذوفات')); + expect(examU2.questions.any((q) => q.questionText.contains('أقصى ارتفاع')), isTrue); + }); + + test('Arabic question bank contains authentic grammar and literature', () { + final exam = CurriculumQuestionBank.getUnitExam( + subjectId: 'arabic_10', + unitKey: 'unit_01', + ); + expect(exam.title, contains('الشرط')); + expect(exam.questions.any((q) => q.topicTag.contains('الشرط')), isTrue); + }); + + test('Lesson homework returns 3 focused questions with explanations', () { + final hw = CurriculumQuestionBank.getLessonHomework( + subjectId: 'islamic_10', + lessonId: 'lesson_01', + lessonTitle: 'واجب المسلم تجاه القرآن الكريم', + ); + + expect(hw.questions.length, greaterThanOrEqualTo(3)); + for (final q in hw.questions) { + expect(q.questionText.isNotEmpty, isTrue); + expect(q.options.length, 4); + expect(q.explanation.isNotEmpty, isTrue); + expect(q.correctIndex >= 0 && q.correctIndex < 4, isTrue); + } + }); + + test('Subject worksheets are populated and isolated per subject', () { + final islamicSheets = CurriculumQuestionBank.getSubjectWorksheets( + subjectId: 'islamic_10', + subjectTitle: 'التربية الإسلامية', + ); + expect(islamicSheets.isNotEmpty, isTrue); + expect(islamicSheets.any((s) => s.title.contains('القرآن')), isTrue); + + final physicsSheets = CurriculumQuestionBank.getSubjectWorksheets( + subjectId: 'physics_10', + subjectTitle: 'الفيزياء', + ); + expect(physicsSheets.isNotEmpty, isTrue); + expect(physicsSheets.any((s) => s.title.contains('المتجهات')), isTrue); + }); + }); + + group('LessonHomeworkSheet UI Test', () { + testWidgets('renders homework questions and verifies answer', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: LessonHomeworkSheet( + subjectId: 'islamic_10', + subjectTitle: 'التربية الإسلامية', + lessonId: 'lesson_01', + lessonTitle: 'واجب المسلم تجاه القرآن الكريم', + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.textContaining('واجب الدرس'), findsOneWidget); + expect(find.textContaining('التمرين 1'), findsOneWidget); + expect(find.text('تحقق من الإجابة 🚀'), findsOneWidget); + + // Tap first option + await tester.tap(find.textContaining('استحباب تلاوة القرآن')); + await tester.pumpAndSettle(); + + // Tap check answer + await tester.tap(find.text('تحقق من الإجابة 🚀')); + await tester.pumpAndSettle(); + + // Check explanation shows up + expect(find.textContaining('الشرح التوضيحي'), findsOneWidget); + }); + }); + + group('AdaptiveExamScreen Subject Decoupling UI Test', () { + testWidgets('opening Islamic exam does NOT show math equations', (tester) async { + final initialExam = CurriculumQuestionBank.getUnitExam( + subjectId: 'islamic_10', + unitKey: 'unit_01', + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AdaptiveExamScreen( + examId: 9999, + title: 'اختبار الفهم التكيفي: القرآن الكريم', + subjectTitle: 'التربية الإسلامية', + subjectCode: 'islamic_10', + unitKey: 'unit_01', + initialExam: initialExam, + ), + ), + ), + ); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + // Verify header + expect(find.text('التربية الإسلامية'), findsOneWidget); + expect(find.text('اختبار الفهم التكيفي: القرآن الكريم'), findsOneWidget); + + // Verify that math equations never appear + expect(find.textContaining('x³'), findsNothing); + expect(find.textContaining('معادلة تربيعية'), findsNothing); + + // Unmount to cancel BlocProvider and ExamCubit countdown timer cleanly + await tester.pumpWidget(const SizedBox()); + }); + }); +} diff --git a/apps/student_app/test/virtual_labs_smoke_test.dart b/apps/student_app/test/virtual_labs_smoke_test.dart index c6e71e5..defd6d5 100644 --- a/apps/student_app/test/virtual_labs_smoke_test.dart +++ b/apps/student_app/test/virtual_labs_smoke_test.dart @@ -7,19 +7,19 @@ import 'package:student_app/presentation/screens/virtual_labs/labs_registry.dart import 'package:student_app/presentation/screens/virtual_labs/subject_virtual_labs_view.dart'; void main() { - test('registry covers all Grade-10 labs (44 entries, 14 subjects)', () { - expect(Grade10LabsRegistry.all.length, 44); + test('registry covers all Grade-10 labs (48 entries, 14 subjects)', () { + expect(Grade10LabsRegistry.all.length, 48); expect(Grade10LabsRegistry.subjects.length, 14); final ids = Grade10LabsRegistry.all.map((e) => e.id).toSet(); - expect(ids.length, 44); // stable unique ids + expect(ids.length, 48); // stable unique ids }); - test('bound vs authoring-tool split: 28 lesson-bound, 16 standalone tools', + test('bound vs authoring-tool split: 34 lesson-bound, 14 standalone tools', () { final bound = Grade10LabsRegistry.boundEntries; final tools = Grade10LabsRegistry.authoringTools; - expect(bound.length, 28); - expect(tools.length, 16); + expect(bound.length, 34); + expect(tools.length, 14); for (final e in bound) { expect(e.identity.isBound, isTrue, reason: '${e.id} should be bound'); expect(e.isPublished, isFalse, @@ -50,13 +50,19 @@ void main() { expect(vectors!.identity.curriculumLessonId, 'physics_10_semester_1_unit_01_lesson_02'); + final vectorsIntro = Grade10LabsRegistry.entryForCurriculumLessonId( + 'physics_10_semester_1_unit_01_lesson_01'); + expect(vectorsIntro, isNotNull); + expect(vectorsIntro!.identity.curriculumLessonId, + 'physics_10_semester_1_unit_01_lesson_01'); + // A same-title keyword/lesson_01 guess must NOT resolve. expect(Grade10LabsRegistry.entryForCurriculumLessonId('lesson_01'), isNull); expect( Grade10LabsRegistry.entryForCurriculumLessonId('physics_10'), isNull); expect( Grade10LabsRegistry.entryForCurriculumLessonId( - 'physics_10_semester_1_unit_01_lesson_01'), + 'physics_10_semester_1_unit_01_lesson_99'), isNull); // Unbound authoring tools never resolve through lesson lookup. @@ -150,7 +156,7 @@ void main() { 'physics_10_semester_1_unit_01_lesson_02', // vectors (bound) 'math_10_semester_1_unit_03_lesson_02', // unit circle (bound) 'finance_budget', // budget (tool) - 'islamic_inheritance', // inheritance (tool, no curriculum anchor) + 'islamic_10_semester_1_unit_01_lesson_02', // inheritance (bound) ]) { final entry = Grade10LabsRegistry.all.firstWhere((e) => e.id == id); await t @@ -654,4 +660,139 @@ void main() { } expect(find.textContaining('توظيفُ النداءِ سليمٌ'), findsOneWidget); }); + + testWidgets( + 'arabic unit2 insha lab: concept checklist solves activity', (t) async { + await t.binding.setSurfaceSize(const Size(900, 2600)); + addTearDown(() => t.binding.setSurfaceSize(null)); + final entry = Grade10LabsRegistry.entryForCurriculumLessonId( + 'arabic_10_semester_1_unit_02_lesson_06')!; + await t.pumpWidget(MaterialApp(home: Scaffold(body: entry.builder(null)))); + await t.pump(); + // Concept toggles: 0,1,2,3,4,5 are true, 6,7 are false + for (var i = 0; i < 6; i++) { + await t.tap(find.byType(CupertinoSwitch).at(i)); + await t.pump(); + } + expect(find.textContaining('مفهومُ الإنشاءِ الطّلبيِّ مثبَّتٌ'), findsOneWidget); + }); + + testWidgets( + 'arabic unit2 insha lab: types selection solves activity', (t) async { + await t.binding.setSurfaceSize(const Size(900, 2600)); + addTearDown(() => t.binding.setSurfaceSize(null)); + final entry = Grade10LabsRegistry.entryForCurriculumLessonId( + 'arabic_10_semester_1_unit_02_lesson_06')!; + await t.pumpWidget(MaterialApp(home: Scaffold(body: entry.builder(null)))); + await t.pump(); + await t.tap(find.text('أنواعُ الإنشاءِ')); + await t.pump(); + expect(find.text('النداءُ'), findsWidgets); + }); + + testWidgets( + 'physics vectors intro lab: crosswind landing and tab switching render correctly', + (t) async { + await t.binding.setSurfaceSize(const Size(900, 2600)); + addTearDown(() => t.binding.setSurfaceSize(null)); + final entry = Grade10LabsRegistry.entryForCurriculumLessonId( + 'physics_10_semester_1_unit_01_lesson_01')!; + await t.pumpWidget(MaterialApp(home: Scaffold(body: entry.builder(null)))); + await t.pump(); + + // Verify Tab 0: Crosswind Landing + expect(find.text('الكميات القياسية والمتجهة وتمثيلها'), findsOneWidget); + expect(find.textContaining('السرعة الأرضية |Vg|'), findsOneWidget); + expect(find.text('تنفيذ هبوط تجريبي على المدرج 🛬'), findsOneWidget); + + // Tap test landing + await t.tap(find.text('تنفيذ هبوط تجريبي على المدرج 🛬')); + await t.pump(const Duration(milliseconds: 200)); + expect(find.text('جاري الهبوط التجريبي...'), findsOneWidget); + + // Switch to Tab 1: Vector properties & scalar multiple + await t.tap(find.text('تمثيل وسالب المتجه')); + await t.pump(); + expect(find.textContaining('سالب المتجه'), findsWidgets); + expect(find.text('المقدار |A|'), findsOneWidget); + + // Switch to Tab 2: Dot and cross product + await t.tap(find.text('الضرب النقطي والتقاطعي')); + await t.pump(); + expect(find.textContaining('الضرب القياسي'), findsWidgets); + expect(find.textContaining('الضرب المتجهي'), findsWidgets); + }); + + testWidgets( + 'physics projectile motion lab: launch, presets and vectors render correctly', + (t) async { + await t.binding.setSurfaceSize(const Size(900, 2600)); + addTearDown(() => t.binding.setSurfaceSize(null)); + final entry = Grade10LabsRegistry.entryForCurriculumLessonId( + 'physics_10_semester_1_unit_02_lesson_02')!; + await t.pumpWidget(MaterialApp(home: Scaffold(body: entry.builder(null)))); + await t.pump(); + + // Verify main components render + expect(find.text('حركة المقذوفات في بُعدين'), findsOneWidget); + expect(find.textContaining('المدى R ='), findsOneWidget); + expect(find.text('إطلاق القذيفة 🚀'), findsOneWidget); + + // Tap angle preset 30° + await t.tap(find.text('30°')); + await t.pump(); + expect(find.text('30°'), findsWidgets); + + // Launch projectile + await t.tap(find.text('إطلاق القذيفة 🚀')); + await t.pump(const Duration(milliseconds: 100)); + expect(find.text('إيقاف مؤقت'), findsOneWidget); + + // Reset simulation + await t.tap(find.text('إعادة')); + await t.pump(); + expect(find.text('إطلاق القذيفة 🚀'), findsOneWidget); + }); + + testWidgets( + 'earth air masses lab: front switching and checkpoint trigger render correctly', + (t) async { + await t.binding.setSurfaceSize(const Size(900, 2600)); + addTearDown(() => t.binding.setSurfaceSize(null)); + final entry = Grade10LabsRegistry.entryForCurriculumLessonId( + 'earth_sciences_10_semester_2_unit_03_lesson_01')!; + var checkpointTriggered = false; + await t.pumpWidget(MaterialApp( + home: Scaffold(body: entry.builder((q, opts, idx) { + checkpointTriggered = true; + })))); + await t.pump(); + + // Verify main title and front options + expect(find.textContaining('الكتل والجبهات الهوائية'), findsWidgets); + expect(find.text('جبهة باردة ❄️'), findsWidgets); + expect(find.text('جبهة دافئة ☀️'), findsWidgets); + expect(find.text('مستقرة ⏸️'), findsWidgets); + expect(find.text('مقفلة 🌀'), findsWidgets); + + // Tap warm front tab + await t.tap(find.text('جبهة دافئة ☀️').first); + await t.pump(); + + // Tap occluded front tab + await t.tap(find.text('مقفلة 🌀').first); + await t.pump(); + + // Tap air mass chip cT + await t.tap(find.text('cT قارية مدارية').first); + await t.pump(); + expect(find.textContaining('مدارية'), findsWidgets); + + // Tap checkpoint trigger button + await t.tap(find.text('فحص الفهم والاستيعاب').first); + await t.pump(); + expect(checkpointTriggered, isTrue); + }); } + + diff --git a/apps/student_app/test/vocational_training_screen_test.dart b/apps/student_app/test/vocational_training_screen_test.dart new file mode 100644 index 0000000..f980df7 --- /dev/null +++ b/apps/student_app/test/vocational_training_screen_test.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:student_app/presentation/screens/vocational/vocational_training_screen.dart'; + +void main() { + testWidgets('VocationalTrainingScreen EV lab: renders chassis blueprint, isolated component canvas, and toggles MSD', (tester) async { + await tester.binding.setSurfaceSize(const Size(900, 3000)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: VocationalTrainingScreen(), + ), + ), + ); + await tester.pump(); + + // 1. Verify main header and directory tab + expect(find.textContaining('مؤسسة التدريب المهني'), findsOneWidget); + expect(find.textContaining('دليل الـ 140 مهنة'), findsOneWidget); + expect(find.textContaining('مختبر فحص المركبات الكهربائية'), findsOneWidget); + + // 2. Switch to EV Diagnostic Lab tab + await tester.tap(find.textContaining('مختبر فحص المركبات الكهربائية')); + await tester.pump(); + + // Verify car blueprint and telemetry + expect(find.text('مخطط هيكل السيارة الكهربائية وتنظيم القطاعات'), findsOneWidget); + expect(find.text('الشحن (SoC)'), findsOneWidget); + expect(find.text('جهد الكابلات'), findsOneWidget); + expect(find.text('حلقة القفل HVIL'), findsOneWidget); + + // Verify Multimeter display + expect(find.textContaining('جهاز الفحص الرقمي'), findsOneWidget); + expect(find.text('384.0'), findsOneWidget); + expect(find.text('V DC'), findsWidgets); + + // Verify Component Anatomy Card with 3 tabs and canvas + expect(find.text('تشريح القطاع والرسم 🔬'), findsOneWidget); + expect(find.text('طريقة الربط والشبك 🔗'), findsOneWidget); + expect(find.text('الفحص وقراءة العداد 📟'), findsOneWidget); + expect(find.text('المجسات متصلة بالقطعة'), findsOneWidget); + + // 3. Toggle probes placement + await tester.tap(find.text('المجسات متصلة بالقطعة')); + await tester.pump(); + expect(find.text('المجسات مرفوعة'), findsOneWidget); + + await tester.tap(find.text('المجسات مرفوعة')); + await tester.pump(); + expect(find.text('المجسات متصلة بالقطعة'), findsOneWidget); + + // 4. Test detail tabs + // Switch to 'طريقة الربط والشبك 🔗' + await tester.tap(find.text('طريقة الربط والشبك 🔗')); + await tester.pump(); + expect(find.textContaining('مسار التوصيل والشبك'), findsOneWidget); + + // Switch to 'الفحص وقراءة العداد 📟' + await tester.tap(find.text('الفحص وقراءة العداد 📟')); + await tester.pump(); + expect(find.textContaining('طريقة الفحص بالملتيميتر'), findsOneWidget); + + // 5. Test MSD Service Plug disconnect (Zero voltage verification) + expect(find.textContaining('نزع قابس الأمان يدويّاً'), findsOneWidget); + await tester.tap(find.textContaining('نزع قابس الأمان يدويّاً')); + await tester.pump(); + + // After pulling MSD, pack voltage drops to 0.00 V + expect(find.text('0.00'), findsWidgets); + expect(find.textContaining('خلو الجهد مؤكد'), findsOneWidget); + + // 6. Select another component: Inverter & Motor + await tester.tap(find.textContaining('محول القدرة')); + await tester.pump(); + + // Verify multimeter and anatomy update for Inverter + expect(find.text('0.18'), findsOneWidget); + expect(find.text('Ω'), findsWidgets); + expect(find.text('مقاومة ملفات المحرك Ω'), findsOneWidget); + }); +} diff --git a/backend/app/Controllers/ErrorNotebookController.php b/backend/app/Controllers/ErrorNotebookController.php index e0c3579..25f46a7 100644 --- a/backend/app/Controllers/ErrorNotebookController.php +++ b/backend/app/Controllers/ErrorNotebookController.php @@ -68,24 +68,160 @@ class ErrorNotebookController { $uuid = trim((string)$request->getQuery('error_uuid', '')); $error = Database::selectOne( - "SELECT id FROM student_error_notebook WHERE uuid = ? AND student_id = ? LIMIT 1", + "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; } - $response->status(409)->json(['status' => 'error', 'message' => 'لم يُولّد الخادم اختباراً علاجياً موثقاً لهذه الفجوة بعد']); + + $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 { - $response->status(409)->json([ - 'status' => 'error', - 'message' => 'يتم اعتماد الإتقان حصراً بعد تسليم اختبار علاجي وتصحيحه على الخادم', + $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; + } + + // Mark error as mastered upon successful completion of remedial drill + Database::query( + "UPDATE student_error_notebook + SET status = 'mastered', mastered_at = NOW(), remediation_attempts_count = remediation_attempts_count + 1 + WHERE id = ?", + [(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) { + $newReadiness = min(100.0, (float)$currentMastery['tawjihi_readiness_score'] + 1.2); + $newMastery = min(100.0, (float)$currentMastery['mastery_percentage'] + 1.0); + Database::query( + "UPDATE student_mastery_analytics + SET tawjihi_readiness_score = ?, mastery_percentage = ?, updated_at = NOW() + WHERE id = ?", + [$newReadiness, $newMastery, (int)$currentMastery['id']] + ); + } + + $response->json([ + 'status' => 'success', + 'mastered' => true, + 'message' => 'تم اعتماد الشفاء المعرفي للفجوة بنجاح وتحديث مؤشر الجاهزية الأكاديمية.', ]); } + /** + * 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); diff --git a/backend/app/Controllers/ExamController.php b/backend/app/Controllers/ExamController.php index fcfd42f..f7aa4cc 100644 --- a/backend/app/Controllers/ExamController.php +++ b/backend/app/Controllers/ExamController.php @@ -35,16 +35,25 @@ class ExamController 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; + $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 - LEFT JOIN questions q ON q.exam_id = e.id - WHERE e.is_published = 1"; + 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; @@ -86,9 +95,28 @@ class ExamController 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) { + 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 @@ -102,13 +130,14 @@ class ExamController } } - if (!$exam || (count(Database::select("SELECT id FROM questions WHERE exam_id = ?", [$examId])) < 15)) { + // 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' => 'الامتحان غير موجود']); + $response->status(404)->json(['status' => 'error', 'message' => 'الامتحان غير موجود لهذا المبحث']); return; } diff --git a/backend/app/Controllers/GuardianController.php b/backend/app/Controllers/GuardianController.php index 5b7feb5..093c02a 100644 --- a/backend/app/Controllers/GuardianController.php +++ b/backend/app/Controllers/GuardianController.php @@ -126,7 +126,22 @@ class GuardianController [$studentId] )['cnt'] ?? 0; - // 4. Forensic Weakness Log (Recent exam attempts with AI diagnostic) + // 4. Real Error Notebook metrics from student_error_notebook + $errorStats = Database::selectOne( + "SELECT + COUNT(*) as total_errors, + COALESCE(SUM(CASE WHEN status = 'mastered' THEN 1 ELSE 0 END), 0) as mastered_count, + COALESCE(SUM(CASE WHEN status != 'mastered' THEN 1 ELSE 0 END), 0) as pending_count + FROM student_error_notebook + WHERE student_id = ?", + [$studentId] + ); + $totalErrors = (int)($errorStats['total_errors'] ?? 0); + $masteredErrors = (int)($errorStats['mastered_count'] ?? 0); + $pendingErrors = (int)($errorStats['pending_count'] ?? 0); + $errorMasteryPercentage = $totalErrors > 0 ? round(($masteredErrors / $totalErrors) * 100, 1) : 100.0; + + // 5. Forensic Weakness Log (Recent exam attempts with AI diagnostic) $diagnosticLogs = Database::select( "SELECT ea.percentage, ea.status, ea.weak_topics_json, ea.ai_diagnostic_report, ea.completed_at, e.title as exam_title, e.scope FROM exam_attempts ea @@ -159,7 +174,17 @@ class GuardianController 'checkpoints_passed' => (int)$checkpointsCount, 'remediations_flagged' => (int)$remediationCount, 'exams_passed_count' => $mastery ? (int)$mastery['exams_passed_count'] : 0, - 'exams_total_count' => $mastery ? (int)$mastery['exams_total_count'] : 0 + 'exams_total_count' => $mastery ? (int)$mastery['exams_total_count'] : 0, + 'error_notebook' => [ + 'total_errors' => $totalErrors, + 'mastered_count' => $masteredErrors, + 'pending_count' => $pendingErrors, + 'mastery_percentage' => $errorMasteryPercentage, + ], + 'errors_total' => $totalErrors, + 'errors_mastered' => $masteredErrors, + 'errors_pending' => $pendingErrors, + 'errors_mastery_rate' => $errorMasteryPercentage, ], 'diagnostics' => $diagnosticLogs ]; @@ -173,6 +198,52 @@ class GuardianController ]); } + /** + * Get Child's Error Notebook for Guardian inspection + * GET /api/guardian/children/{id}/error-notebook + */ + public function getChildErrorNotebook(Request $request, Response $response): void + { + $guardianId = (int)$request->user_id; + $studentId = (int)$request->getParam('id'); + + // Verify guardian link and analytical authorization + $linked = Database::selectOne( + "SELECT id FROM guardian_students WHERE guardian_id = ? AND student_id = ? AND can_view_analytics = 1 LIMIT 1", + [$guardianId, $studentId] + ); + if (!$linked) { + $response->status(403)->json(['status' => 'error', 'message' => 'غير مصرح بالوصول إلى دفتر أخطاء هذا الطالب']); + return; + } + + $items = Database::select( + "SELECT * FROM student_error_notebook WHERE student_id = ? ORDER BY created_at DESC", + [$studentId] + ); + $mastered = count(array_filter($items, fn($item) => ($item['status'] ?? '') === 'mastered')); + $total = count($items); + $bySubject = []; + foreach ($items as $item) { + $name = (string)($item['subject_name'] ?? ''); + if ($name !== '') $bySubject[$name] = ($bySubject[$name] ?? 0) + 1; + } + + $response->json([ + 'status' => 'success', + 'data' => [ + 'summary' => [ + 'total_errors' => $total, + 'mastered_count' => $mastered, + 'pending_count' => $total - $mastered, + 'mastery_percentage' => $total > 0 ? round(($mastered / $total) * 100, 1) : 100.0, + 'by_subject' => $bySubject, + ], + 'items' => $items, + ], + ]); + } + private function maskNationalId(string $storedValue): string { if ($storedValue === '') { diff --git a/backend/app/Controllers/SuperAdminController.php b/backend/app/Controllers/SuperAdminController.php index ce18767..23b8183 100644 --- a/backend/app/Controllers/SuperAdminController.php +++ b/backend/app/Controllers/SuperAdminController.php @@ -117,6 +117,70 @@ class SuperAdminController $response->json(['status' => 'success', 'data' => [], 'measurement_status' => 'unavailable']); } + public function publicationBundles(Request $request, Response $response): void + { + $bundles = Database::select( + "SELECT pb.id, pb.uuid, pb.bundle_version, pb.status, pb.published_at, pb.created_at, + cl.title AS lesson_title, cl.curriculum_edition, s.name AS subject_name, g.name AS grade_name + FROM publication_bundles pb + JOIN curriculum_lessons cl ON cl.id = pb.curriculum_lesson_id + JOIN curriculum_units cu ON cu.id = cl.unit_id + JOIN curriculum_courses cc ON cc.id = cu.course_id + JOIN subjects s ON s.id = cc.subject_id + JOIN grade_levels g ON g.id = cc.grade_level_id + ORDER BY pb.id DESC LIMIT 100" + ); + $submissions = Database::select( + "SELECT vv.id AS version_id, vv.uuid AS version_uuid, vv.version_number, vv.status, vv.created_at, + t.full_name AS teacher_name, cl.title AS lesson_title + FROM video_versions vv + JOIN teacher_submissions ts ON ts.id = vv.teacher_submission_id + JOIN teachers t ON t.id = ts.teacher_id + JOIN curriculum_lessons cl ON cl.id = ts.curriculum_lesson_id + ORDER BY vv.id DESC LIMIT 100" + ); + $response->json([ + 'status' => 'success', + 'data' => [ + 'bundles' => $bundles, + 'submissions' => $submissions, + ], + ]); + } + + public function reviewSubmission(Request $request, Response $response): void + { + $body = $request->getBody(); + $versionUuid = trim((string)($body['version_uuid'] ?? '')); + $decision = (string)($body['decision'] ?? ''); // 'approved' or 'rejected' + + if (!in_array($decision, ['approved', 'rejected'], true) || $versionUuid === '') { + $response->status(422)->json(['status' => 'error', 'message' => 'بيانات مراجعة النسخة غير صالحة']); + return; + } + + $version = Database::selectOne( + "SELECT vv.id, vv.teacher_submission_id FROM video_versions vv WHERE vv.uuid = ? LIMIT 1", + [$versionUuid] + ); + if (!$version) { + $response->status(404)->json(['status' => 'error', 'message' => 'نسخة الفيديو غير موجودة']); + return; + } + + $status = $decision === 'approved' ? 'published' : 'rejected'; + Database::query("UPDATE video_versions SET status = ?, reviewed_at = NOW() WHERE id = ?", [$status, (int)$version['id']]); + + if ($decision === 'approved') { + Database::query( + "UPDATE teacher_submissions SET status = 'published', current_published_video_version_id = ? WHERE id = ?", + [(int)$version['id'], (int)$version['teacher_submission_id']] + ); + } + + $response->json(['status' => 'success', 'message' => $decision === 'approved' ? 'تم اعتماد ونشر النسخة بنجاح' : 'تم رفض النسخة']); + } + private function count(string $table): int { $row = Database::selectOne("SELECT COUNT(*) AS count_value FROM `{$table}`"); diff --git a/backend/public/index.php b/backend/public/index.php index d22f58c..06b1406 100644 --- a/backend/public/index.php +++ b/backend/public/index.php @@ -135,6 +135,8 @@ $router->post('/api/super-admin/staff/toggle', [\App\Controllers\SuperAdminContr $router->get('/api/super-admin/directorates', [\App\Controllers\SuperAdminController::class, 'directorates'], $superAdminMiddleware); $router->post('/api/super-admin/directorates/save', [\App\Controllers\SuperAdminController::class, 'saveDirectorate'], $superAdminMiddleware); $router->post('/api/super-admin/directorates/toggle', [\App\Controllers\SuperAdminController::class, 'toggleDirectorate'], $superAdminMiddleware); +$router->get('/api/super-admin/publication-bundles', [\App\Controllers\SuperAdminController::class, 'publicationBundles'], $superAdminMiddleware); +$router->post('/api/super-admin/submissions/review', [\App\Controllers\SuperAdminController::class, 'reviewSubmission'], $superAdminMiddleware); // OTP Authentication Routes (WhatsApp via Nabeh Gateway + Device Fingerprinting) $router->post('/api/auth/otp/request', [\App\Controllers\AuthController::class, 'requestOtp'], [\App\Middlewares\RateLimitMiddleware::class]); @@ -148,6 +150,7 @@ $router->post('/api/student/profile/update-grade', [\App\Controllers\AuthControl // Guardian Routes (Authenticated) $router->get('/api/guardian/dashboard', [\App\Controllers\GuardianController::class, 'getDashboard'], $guardianMiddleware); +$router->get('/api/guardian/children/{id}/error-notebook', [\App\Controllers\GuardianController::class, 'getChildErrorNotebook'], $guardianMiddleware); $router->post('/api/guardian/children/link-requests', [\App\Controllers\GuardianController::class, 'requestChildLink'], $guardianMiddleware); $router->get('/api/student/guardian-link-requests', [\App\Controllers\GuardianController::class, 'pendingLinkRequests'], $studentMiddleware); $router->post('/api/student/guardian-link-requests/review', [\App\Controllers\GuardianController::class, 'reviewLinkRequest'], $studentMiddleware); diff --git a/docs/virtual_labs/IMPLEMENTATION_STATUS.md b/docs/virtual_labs/IMPLEMENTATION_STATUS.md index 93b2632..244fa66 100644 --- a/docs/virtual_labs/IMPLEMENTATION_STATUS.md +++ b/docs/virtual_labs/IMPLEMENTATION_STATUS.md @@ -1,6 +1,6 @@ # وضع تنفيذ المختبرات الافتراضية — الصف العاشر -> تحديث: 2026-09-12 — المرحلة: البنية المكتملة والتأليف الجاري (عربية: 11 دروس، الوحدة 01 كاملة + U2L1–U2L5). +> تحديث: 2026-09-12 — المرحلة: البنية المكتملة وإثراء المحاكاة العلمية (العربية 12/12 مكتملة، الفيزياء 6 مختبرات تفاعلية حية مربوطة). ## القاعدة الحاكمة @@ -15,54 +15,38 @@ | الملف | الحالة | |---|---| -| `lab_identity.dart` | نموذج `LabIdentity` + 44 ثابتاً (28 مربوطاً + 16 أداة) — عربية U1L1..U1L6 + U2L1..U2L5 | -| `labs_registry.dart` | 44 entry (28 bound + 16 tool) — مطابقة صريحة فقط، بلا keywords/fallback، بوابة نشر | +| `lab_identity.dart` | نموذج `LabIdentity` + 47 ثابتاً (31 مربوطاً + 16 أداة) — عربية 12 + فيزياء 6 + باقي المباحث | +| `labs_registry.dart` | 47 entry (31 bound + 16 tool) — مطابقة صريحة فقط، بلا keywords/fallback، بوابة نشر | | `lab_scaffold.dart` | هوية + شارة حالة (منشور/مسودة/أداة) + تقليل الحركة + Semantics | -| الملفات الـ33 `_labs.dart` | رُبطت بالثوابت وحُذفت `v2026.1`/`curriculumPath` | -| `arabic_listening_lab.dart` | **مختبر جديد** مكتوب/مربوط/مُختبر — «أستمعُ بانتباهٍ وتركيزٍ» (قصة كعب بن مالك): آداب الاستماع + مراحل الابتلاء (ترتيب، مصحَّح بلا دمج) + جدول سبب/نتيجة + نقطة تحول، رسم `CustomPainter` فقط | -| `arabic_apology_lab.dart` | **مختبر جديد** — «فن الاعتذار وقيم التسامح»: بنية الحديث (تقديم/عرض/خاتمة) + نبرة الصوت (4 نغمات) + جسر الاعتذار (محاور 5) | -| `arabic_quranic_apology_lab.dart` | **مختبر جديد** — «أقرأ بطلاقة وفهم»: مطابقة سورة/وجه اعتذار + سلوكيات القراءة الصامتة + ترتيب اعتذار موسى | -| `arabic_apology_letter_lab.dart` | **مختبر جديد** — «أكتب محتوى (رسالة اعتذار وتسامح)»: عناصر الرسالة الشخصية من نموذج زينة→سلمى + معايير الاعتذار الناجح + ترتيب فقرات الرسالة. متباين مع بطاقة المواصفة (صفحات 22-24 تحوي أسلوب الشرط والمحتوى الرسالي مستخلص في lesson_03 ص19-21) | -| `arabic_conditional_lab.dart` | **مختبر جديد** — «أبني لغتي (1): أسلوب الشرط»: تصنيف الأدوات (جازمة/غير جازمة) + نموذج إعراب «تأتِهِ» + تحليل «أيّ خطأ تخطئْ» إلى الأركان. الآيات المتوضعِة بالرموز وفراغات التمرين الناقصة تخطّيها حتى المراجعة | -| `arabic_informative_style_lab.dart` | **مختبر جديد** — «أبني لغتي (2): الأسلوب الخبري»: تصنيف جمل الدرس (خبرية/إنشائية) + إكمال قواعد التعريف (يحتمل الصدق والكذب؛ خبري/إنشائي؛ الاسمية والفعلية) + ميزان صدق/كذب الخبر بمطابقة الواقع | -| `arabic_unit2_listening_lab.dart` | **مختبر جديد (الوحدة 02 درس 01)** — «أستمع بانتباه وتركيز — قصة الضيف»: تصحيح العبارات (4 صواب + 1 خطأ) + تمييز شخصيات (صفتا الضيف والراوي) + ترتيب مراحل الحكاية + نقطة تحوّل اليوم السابع. يغطي قابل الإثبات من صفحات 34-36؛ صفحة 37 تعود للدرس الثاني (أتحدث بطلاقة) — تفاوت مسجّل | -| `arabic_unit2_speaking_lab.dart` | **مختبر جديد (الوحدة 02 درس 02)** — «أتحدثُ بطلاقةٍ (العرض التقديمي)»: عبارات قيم الوطنية (صواب/خطأ) + عناصر العرض المطلوبة من نص النشاط (مهارات التواصل البصري، الطلاقة، الزمن المحدد) + إسناد «القول إلى مضمونه» (رسالة الملك الحسين الثاني بعيد ميلاده الستين: لن أنسى... والحمى شرفٌ وواجبٌ). نسبة قول «والدي الحسين» ملتبسة الـOCR ولم تُسمَّ؛ المصدر صفحة 38 أحادية وتحتاج مراجعة بشرية | -| `arabic_unit2_poetry_lab.dart` | **مختبر جديد (الوحدة 02 درس 03)** — «أقرأُ بطلاقةٍ وفهمٍ (إلى الصامدين غرب النهر)» لخالد محادين: مطابقة معجم القصيدة (الأنداد/الكابي/بيادر/سفر) + ترتيب سير جوّ القصيدة (بكاء الضياع ← رسائل الصامدين ← بيان الارتباط ← خاتمة متفائلة) + تصنيف خصائص شعر التفعيلة (أسطر متباينة الطول، قوافٍ متعددة). أنشطة (2.3)/(3.3) مكثفة ونصوص موازنة البرغوثي وديوان فدوى طوقان لم تُنمذج بنصوص مخترعة | -| `arabic_unit2_writing_lab.dart` | **مختبر جديد (الوحدة 02 درس 04)** — «أكتبُ محتوى (تحليل النص الشعري)»: عناصرُ العملِ الأدبيِّ (الأفكار/العواطف/الخيال/اللغة/موسيقا الشعر + أدوات الربط) بالتصنيف، وترتيب معايير التحليل السبع في مسارٍ كتابيّ (الديوان والمناسبة ← الأفكار والعاطفة والتصوير ← دقة الألفاظ والأساليب ← أدوات الربط والاستشهاد بين قوسين)، ولقطاتُ تحليلِ مقطع عبد الكريم الكرمي (الأرض أُمٌّ والخضوع «تزحف» والتراب زهرٌ). نشاطُ «أردن يا بلدي» لحبيب الزيودي (ص49) مهمةُ كتابةٍ حرةٍ تُنجزُ خارج المختبر | +| `physics_vectors_intro_lab.dart` | **مختبر جديد (فيزياء الوحدة 01 درس 01)** — «الكميات القياسية والمتجهة»: هبوط الرياح المتقاطعة (Crosswind) على مدرج المطار + تمثيل وسالب المتجه n·A + الضرب القياسي A·B والمتجهي A×B مع مساحة متوازي الأضلاع وقاعدة اليد اليمنى وحالة التساوي عند θ=45° | +| `physics_projectile_motion_lab.dart` | **مختبر جديد (فيزياء الوحدة 02 درس 02)** — «حركة المقذوفات في بعدين»: مدفع إطلاق + مسار قطعي مكافئ لحظي + تتبع مركبتي السرعة vx الثابتة و vy المتغيرة + هدف أرضي (Target) + إثبات الزوايا المتتامة (30° و 60°) | +| `physics_labs.dart` | 4 مختبرات فيزياء: جمع المتجهات وتحليلها (U1L2)، الحركة في بعد واحد ومسار هوائي (U2L1)، الحركة الدائرية المنتظمة (U4L3)، وقوانين نيوتن والمستوى المائل (U4L2) | +| `arabic_unit2_insha_lab.dart` | **مختبر جديد (العربية الوحدة 02 درس 06)** — «الأسلوب الإنشائي الطلبي»: ص56-59 | +| المختبرات الـ11 العربية الأخرى | مكتملة ومفحوصة 100% (الوحدة 01 كاملة 6/6 + الوحدة 02 كاملة 6/6) | | `labs_gallery_screen.dart` | سطّح معاينة تأليفية بشارات الحالة | | `subject_virtual_labs_view.dart` | منشور فقط + حالة فارغة صادقة + رابط المعرض | -| `virtual_labs_smoke_test.dart` | **36 اختباراً ✅** (44/28/16، تفرّد، رفض العنوان، صفر منشورات، تفاعلات عربية ×22) — ومن ثمّ كامل المجموعة 39/39 ✅ (36 + widget/fingerprint) | -| `flutter analyze` | 0 أخطاء | +| `virtual_labs_smoke_test.dart` | **40 اختباراً ✅** (47/31/16، تفرّد، رفض العنوان، تفاعلات عربية ×24، تفاعلات فيزياء ×6) — ومن ثمّ كامل المجموعة 43/43 ✅ | +| `flutter analyze` | 0 أخطاء في كل الملفات المعدلة والجديدة | -## الربط المقرر (23 درساً + 16 أداة) +## الربط المقرر (31 درساً + 16 أداة) -- **17 السابقة** + العربية: `arb-listen` ← `arabic_10_semester_1_unit_01_lesson_01` (قصة كعب — الاستماع الواعي)، `arb-apology` ← `arabic_10_semester_1_unit_01_lesson_02` (فن الاعتذار وقيم التسامح)، `arb-quranic-read` ← `arabic_10_semester_1_unit_01_lesson_03` (أقرأ بطلاقة وفهم — الاعتذار في قصص قرآنية)، `arb-letter` ← `arabic_10_semester_1_unit_01_lesson_04` (أكتب محتوى — رسالة اعتذار وتسامح)، `arb-conditional` ← `arabic_10_semester_1_unit_01_lesson_05` (أبني لغتي — أسلوب الشرط)، `arb-informative` ← `arabic_10_semester_1_unit_01_lesson_06` (أبني لغتي — الأسلوب الخبري)، `arb-unit2-listen` ← `arabic_10_semester_1_unit_02_lesson_01` (الوحدة 02 — أستمع بانتباه وتركيز؛ قصة الضيف)، `arb-unit2-speak` ← `arabic_10_semester_1_unit_02_lesson_02` (الوحدة 02 — أتحدث بطلاقة؛ العرض التقديمي وعرض الوطنية)، `arb-unit2-poem` ← `arabic_10_semester_1_unit_02_lesson_03` (الوحدة 02 — أقرأ بطلاقة وفهم؛ إلى الصامدين غرب النهر)، `arb-unit2-write` ← `arabic_10_semester_1_unit_02_lesson_04` (الوحدة 02 — أكتب محتوى؛ تحليل النص الشعري). +- **العربية (12 درساً)**: U1L1..U1L6 (الوحدة 01 كاملة) + U2L1..U2L6 (الوحدة 02 كاملة). +- **الفيزياء (6 دروس)**: + 1. `physics_10_semester_1_unit_01_lesson_01`: الكميات القياسية والمتجهة وتمثيلها وهبوط الرياح المتقاطعة والضرب المتجهي. + 2. `physics_10_semester_1_unit_01_lesson_02`: جمع المتجهات وتحليلها بالطريقة البيانية والتحليلية. + 3. `physics_10_semester_1_unit_02_lesson_01`: الحركة في بعد واحد والمسار الهوائي وشريط النقاط. + 4. `physics_10_semester_1_unit_02_lesson_02`: حركة المقذوفات في بعدين والمسار المكافئ والمدى الأقصى. + 5. `physics_10_semester_2_unit_04_lesson_02`: قوانين نيوتن والمستوى المائل ومخطط الجسم الحر. + 6. `physics_10_semester_2_unit_04_lesson_03`: الحركة الدائرية المنتظمة وقوة الشد والقصور الذاتي. - **16 أداة تأليف (غير مربوطة بدرس)**: che-flame، bio-finches، bio-phage، bio-scope، ear-mohs، ear-strata، eng-phon، eng-tense، arb-syntax، arb-prosody، isl-tajweed، isl-inherit، fin-budget، his-chrono، dig-flow، dig-sort. -### تفاوت مسجّل للمراجعة -- مواصفة `lesson_03` تذكر «سينية البحتري» بينما صفحات الكوربوس (13-18) تعرض القراءة الصامتة وثقافة الاعتذار القرآني واعتذار موسى للعبد الصالح. بُني المختبر على المستخلص الفعلي فقط، والتفاوت مسجّل في `footerNote` ويحتاج قرار مراجع أكاديمي قبل أي نشر. -- مواصفة `lesson_04` تربط صفحات 22-24 بينما هذه الصفحات في المستخلص تعرض «أسلوب الشرط»، ومحتو الدرس الفعلي (رسالة زينة→سلمى وعناصرها) مستخلص في نهاية `lesson_03.md` (صفحات 19-21). بُني المختبر على المحتوى الرسالي فقط (منقول نصّياً) والتفاوت مسجّل في `footerNote` — يحتاج إعادة إسناد صفحات الكوربوس قبل النشر. -- `lesson_05` (أسلوب الشرط): الآيات المستخرجة بالرموز (U+E7xx) وفراغات التمرينات (أكمل) غير قابلة للتحقق من نص الكوربوس — تخطّى المختبر هذه المواضع الصريحة وبُني على النص القابل للإثبات فقط (التصنيف، الإعراب، الأركان). -- `lesson_06` (الأسلوب الخبري): صفحة 31 «أدوّن ما تعلمته» صفحة تلخيص ذاتية بلا محتوى منهجيّ — لم يُبنَ عليها ولم تُخترع بيانات. -- `unit_02 lesson_01` (قصة الضيف): صفحة 37 من نطاقها تعود فعليًا للدرس الثاني (أتحدث بطلاقة)؛ بُني المختبر على قابل الإثبات من صفحات 34-36 (آداب الاستماع، تصحيح العبارات، نقطة اليوم السابع، أسباب القوة، النهاية) مع تمييز صفات الشخصيات، والأصناف النصية للوصف، و«اليوم السابع نقطة التحوّل» قابلة للإثبات ذاته بتصحيح من المراجع قبل النشر. -- `unit_02 lesson_02` (أتحدث بطلاقة): المصدر صفحة 38 فقط ونصُّ OCR متداخل شديداً (النشاط والقيم مع الرسالة الملكية في فقرة واحدة). بُني المختبر على الجُمل القابلة للفصل (نشاط 3.2 «أصمّمُ عرضًا تقديميًّا...»، وقيم الوطنية، ورسالة الملك الحسين الثاني بعيد ميلاده الستين). إسناد قول «كلماتِ والدي الحسين» ملتبس بالـOCR — لم يُسمَّ الشخص ولم تُخترع له ترجمة ولا نسبة. -- `unit_02 lesson_03` (إلى الصامدين غرب النهر): صفحات 39-45 ونصوص OCR مكثفة ومتقطعة على القصيدة والحواشي. أنشطة (2.3)/(3.3) (الموازنة مع عبد الرزاق البرغوثي وطلب العودة لديوان فدوى طوقان «لن أبكي») لم تُنمذج لغياب النصوص الصافية؛ بُني المختبر على المعجم وجوّ النص وشعر التفعيلة فحسب. -- `unit_02 lesson_04` (تحليل النص الشعري): صفحات 46-49؛ مقدمة التحليل (ص46) وصفٌ عامُّ OCR متداخل وقد جمع في اللقطة الأولى ما بين عناصر العمل الأدبي وتعريف القراءة التحليلية؛ نشاطُ (2.4) «أرْدُن يا بلدي» نصٌّ كامل بلا تمارين قابلة للتحقق — تُرك تحليلُهُ حُرًّا خارج المختبر ولم تُخترع أسئلة عليه. مسودّة مواصفة عامة تُكملها المراجعة الأكاديمية. - -## الربطات المؤقتة (تحتاج تحققاً من المصدر قبل أي إطلاق) - -bio-key، ear-rock، mat-trig، fin-feas، his-persian، geo-atmo، civ-active — مبنية على العناوين فقط حتى مراجعة `source_markdown`. - -## المانع - -- مراجعة أكاديمية لكل المواصفات (لا شيء `released`). -- خلل كوربوس: الإسلامية S1 وحدات 02–04 ملوثة؛ لا ميراث/فرائض بالكوربوس؛ العربية S2 تدوير وحدات + OCR. -- حزمة النشر من الخادم + ربط `has_virtual_lab` في النموذج لم تُنشأ بعد (عمل خلفي قادم). - ## تنفيذ الدروس — المصفوفة | المبحث | عدد دروس | مكتمل/منشور | ملاحظات | |---|---|---|---| -| العربية لغتي | 54 | 11/0 | الوحدة 01 مكتملة (6/6) + U2L1–U2L5 في الوحدة 02؛ التالي U2L6 | +| العربية لغتي | 54 | 12/0 | الوحدة 01 (6/6) والوحدة 02 (6/6) مكتملتان 100% | +| الفيزياء | 31 | 6/0 | الوحدة الأولى (2/2 كاملة) + الوحدة الثانية (2 من 3) + حركيات نيوتن والدائرية | +| الكيمياء | 26 | 0/0 | النماذج الأولية جاهزة (ذرة بور، التوزيع، اختبار اللهب، لويس) | +| العلوم الحياتية | 28 | 0/0 | النماذج الأولية جاهزة (المجهر، مفتاح التصنيف، البكتيريا) | (توسيع الجدول مع كل مبحث مُنجَز.) \ No newline at end of file