Update Saqel Platform: 2026-09-08 13:43:36
This commit is contained in:
@@ -33,6 +33,7 @@ class AuthRepository {
|
||||
'phone_number': phoneNumber,
|
||||
'otp': otp,
|
||||
'role': role,
|
||||
'device_fingerprint': 'saqel_student_flutter',
|
||||
},
|
||||
requiresAuth: false,
|
||||
);
|
||||
|
||||
@@ -75,7 +75,7 @@ class CurriculumRepository {
|
||||
}
|
||||
});
|
||||
|
||||
return available.isNotEmpty ? available : GradeLevelModel.allK12Grades;
|
||||
return available;
|
||||
}
|
||||
|
||||
/// Fetch subjects for a specific grade level dynamically from Live Backend
|
||||
@@ -98,12 +98,6 @@ class CurriculumRepository {
|
||||
final shortKey = gradeLevel.replaceAll('grade_', '');
|
||||
if (tree.containsKey(shortKey) && tree[shortKey] is Map) {
|
||||
gradeData = Map<String, dynamic>.from(tree[shortKey]);
|
||||
} else if (tree.isNotEmpty) {
|
||||
// Fall back to first available grade if requested grade has no uploaded content yet
|
||||
final firstKey = tree.keys.first;
|
||||
if (tree[firstKey] is Map) {
|
||||
gradeData = Map<String, dynamic>.from(tree[firstKey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,245 +1,45 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../models/error_notebook_model.dart';
|
||||
|
||||
/// ==============================================================================
|
||||
/// SAQEL ENTERPRISE - ERROR NOTEBOOK REPOSITORY & REMEDIATION SERVICE
|
||||
/// ==============================================================================
|
||||
class ErrorNotebookRepository {
|
||||
static const String baseUrl = 'http://127.0.0.1:8000';
|
||||
final ApiClient _api;
|
||||
|
||||
/// Fetch Error Notebook items & statistics
|
||||
Future<Map<String, dynamic>> getErrorNotebook({
|
||||
String? subject,
|
||||
String? status,
|
||||
}) async {
|
||||
try {
|
||||
final uri = Uri.parse('$baseUrl/api/student/error-notebook').replace(
|
||||
queryParameters: {
|
||||
if (subject != null && subject.isNotEmpty) 'subject': subject,
|
||||
if (status != null && status.isNotEmpty) 'status': status,
|
||||
},
|
||||
);
|
||||
ErrorNotebookRepository({ApiClient? api}) : _api = api ?? ApiClient();
|
||||
|
||||
final response = await http
|
||||
.get(uri, headers: {'Accept': 'application/json'})
|
||||
.timeout(const Duration(seconds: 3));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded['status'] == 'success' && decoded['data'] != null) {
|
||||
final data = decoded['data'];
|
||||
final summary = ErrorNotebookSummary.fromJson(data['summary'] ?? {});
|
||||
final rawItems = data['items'] as List? ?? [];
|
||||
final items = rawItems
|
||||
.map((e) => ErrorNotebookItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
return {
|
||||
'summary': summary,
|
||||
'items': items,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Fallback to high-fidelity local data
|
||||
}
|
||||
|
||||
return _getLocalFallbackData();
|
||||
}
|
||||
|
||||
/// Fetch 3-question targeted remediation drill for a troubled concept
|
||||
Future<List<RemedialQuestion>> getRemediationQuiz({
|
||||
required String errorUuid,
|
||||
required String topicName,
|
||||
}) async {
|
||||
try {
|
||||
final uri = Uri.parse('$baseUrl/api/student/error-notebook/remediation-quiz').replace(
|
||||
queryParameters: {
|
||||
'error_uuid': errorUuid,
|
||||
'topic_name': topicName,
|
||||
},
|
||||
);
|
||||
|
||||
final response = await http
|
||||
.get(uri, headers: {'Accept': 'application/json'})
|
||||
.timeout(const Duration(seconds: 3));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded['status'] == 'success' && decoded['data'] != null) {
|
||||
final rawQuestions = decoded['data']['questions'] as List? ?? [];
|
||||
return rawQuestions
|
||||
.map((q) => RemedialQuestion.fromJson(q as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Local fallback
|
||||
}
|
||||
|
||||
return _getLocalFallbackRemediationQuestions(topicName);
|
||||
}
|
||||
|
||||
/// Mark error as mastered upon completing remedial drill
|
||||
Future<bool> resolveError(String errorUuid) async {
|
||||
try {
|
||||
final uri = Uri.parse('$baseUrl/api/student/error-notebook/resolve');
|
||||
final response = await http.post(
|
||||
uri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: json.encode({'error_uuid': errorUuid}),
|
||||
).timeout(const Duration(seconds: 3));
|
||||
|
||||
return response.statusCode == 200;
|
||||
} catch (_) {
|
||||
return true; // Optimistic update
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getLocalFallbackData() {
|
||||
final items = [
|
||||
ErrorNotebookItem(
|
||||
id: 1,
|
||||
uuid: 'err-phy-001',
|
||||
subjectId: 'physics_10',
|
||||
subjectName: 'الفيزياء',
|
||||
topicName: 'جمع وتحليل المتجهات والضرب القياسي',
|
||||
sourceType: 'socratic_checkpoint',
|
||||
questionText:
|
||||
'متجهان A و B مقدار كل منهما 6 وحدات، والزاوية بينهما 90 درجة. ما حاصل ضربهما القياسي (A · B)؟',
|
||||
studentWrongAnswer: '36 وحدة',
|
||||
correctAnswer: 'صفر',
|
||||
socraticHint:
|
||||
'تذكر أن الضرب القياسي يعتمد على جيب التمام: A · B = |A| |B| cos(θ). وجيب تمام الزاوية 90 درجة يساوي صفراً، لذلك ينعدم الضرب القياسي لمتجهين متعامدين تماماً.',
|
||||
errorCategory: 'conceptual',
|
||||
status: 'pending_remediation',
|
||||
remediationAttemptsCount: 0,
|
||||
createdAt: 'منذ يومين',
|
||||
),
|
||||
ErrorNotebookItem(
|
||||
id: 2,
|
||||
uuid: 'err-math-002',
|
||||
subjectId: 'math_10',
|
||||
subjectName: 'الرياضيات',
|
||||
topicName: 'المعنى الهندسي للمشتقة الأولى وميل المماس',
|
||||
sourceType: 'adaptive_exam',
|
||||
questionText:
|
||||
'ما هو التفسير الهندسي للمشتقة الأولى f\'(x₀) عند النقطة (x₀, y₀) الواقعة على منحنى الاقتران؟',
|
||||
studentWrongAnswer: 'معادلة المستقيم القاطع المار بالنقطتين',
|
||||
correctAnswer: 'ميل خط المماس لمنحنى الاقتران عند تلك النقطة',
|
||||
socraticHint:
|
||||
'القاطع يحتاج نقطتين، ولكن بأخذ النهاية عندما تقترب النقطتان من بعضهما، يتحول القاطع إلى مماس، وتكون المشتقة الأولى هي ميل هذا المماس حصراً.',
|
||||
errorCategory: 'conceptual',
|
||||
status: 'pending_remediation',
|
||||
remediationAttemptsCount: 1,
|
||||
createdAt: 'منذ 3 أيام',
|
||||
),
|
||||
ErrorNotebookItem(
|
||||
id: 3,
|
||||
uuid: 'err-eng-003',
|
||||
subjectId: 'english_10',
|
||||
subjectName: 'اللغة الإنجليزية',
|
||||
topicName: 'Definite & Indefinite Articles (a, an, the)',
|
||||
sourceType: 'unit_exam',
|
||||
questionText:
|
||||
'Choose the correct article: "Dr. Zaid is ____ honest researcher who dedicated his life to education."',
|
||||
studentWrongAnswer: 'a',
|
||||
correctAnswer: 'an',
|
||||
socraticHint:
|
||||
'We choose (an) based on the vowel SOUND, not the spelling letter! Since "honest" starts with a silent "h" and a vowel sound (/ˈɒn.ɪst/), we must use "an honest".',
|
||||
errorCategory: 'rushed',
|
||||
status: 'mastered',
|
||||
remediationAttemptsCount: 2,
|
||||
masteredAt: 'اليوم',
|
||||
createdAt: 'منذ 4 أيام',
|
||||
),
|
||||
ErrorNotebookItem(
|
||||
id: 4,
|
||||
uuid: 'err-arb-004',
|
||||
subjectId: 'arabic_10',
|
||||
subjectName: 'اللغة العربية',
|
||||
topicName: 'إنّ وأخواتها وأنواع الخبر',
|
||||
sourceType: 'socratic_checkpoint',
|
||||
questionText: 'في جملة (لعلّ النصرَ قريبٌ)، ما إعراب كلمة (النصرَ)؟',
|
||||
studentWrongAnswer: 'فاعل مرفوع بالضمة',
|
||||
correctAnswer: 'اسم لعلّ منصوب وعلامة نصبه الفتحة الظاهرة',
|
||||
socraticHint:
|
||||
'لعلّ من أخوات إنّ، وهي حروف ناسخة تدخل على الجملة الاسمية فتنصب المبتدأ ويسمى اسمها، وترفع الخبر ويسمى خبرها.',
|
||||
errorCategory: 'conceptual',
|
||||
status: 'mastered',
|
||||
remediationAttemptsCount: 1,
|
||||
masteredAt: 'أمس',
|
||||
createdAt: 'منذ 5 أيام',
|
||||
),
|
||||
];
|
||||
|
||||
final summary = ErrorNotebookSummary(
|
||||
totalErrors: 4,
|
||||
masteredCount: 2,
|
||||
pendingCount: 2,
|
||||
masteryPercentage: 50.0,
|
||||
bySubject: {
|
||||
'الفيزياء': 1,
|
||||
'الرياضيات': 1,
|
||||
'اللغة الإنجليزية': 1,
|
||||
'اللغة العربية': 1,
|
||||
Future<Map<String, dynamic>> 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<String, dynamic>.from(decoded['data'] as Map? ?? const {});
|
||||
final items = (data['items'] as List? ?? const [])
|
||||
.map((item) => ErrorNotebookItem.fromJson(Map<String, dynamic>.from(item as Map)))
|
||||
.toList();
|
||||
return {
|
||||
'summary': summary,
|
||||
'summary': ErrorNotebookSummary.fromJson(Map<String, dynamic>.from(data['summary'] as Map? ?? const {})),
|
||||
'items': items,
|
||||
};
|
||||
}
|
||||
|
||||
List<RemedialQuestion> _getLocalFallbackRemediationQuestions(String topic) {
|
||||
return [
|
||||
RemedialQuestion(
|
||||
id: 1,
|
||||
question:
|
||||
'إذا كانت محصلة القوى المؤثرة على جسم تساوي صفراً (ΣF = 0)، فماذا يحدث لحركته؟',
|
||||
options: [
|
||||
'يتوقف الجسم فوراً عن الحركة في جميع الأحوال',
|
||||
'يتحرك بتسارع ثابت متزايد',
|
||||
'يبقى ساكناً أو يستمر بالحركة بسرعة متجهة ثابتة في خط مستقيم',
|
||||
'تتناقص سرعته تدريجياً حتى يتوقف',
|
||||
],
|
||||
correctIndex: 2,
|
||||
explanation:
|
||||
'هذا نص القانون الأول لنيوتن (القصور الذاتي): الجسم يحافظ على حالته الحركية ما لم تؤثر عليه قوة محصلة.',
|
||||
),
|
||||
RemedialQuestion(
|
||||
id: 2,
|
||||
question:
|
||||
'أثرت قوة أفقية مقدارها 20 نيوتن على جسم كتلته 4 كغ على سطح أملس. ما هو تسارع الجسم؟',
|
||||
options: [
|
||||
'5 م/ث²',
|
||||
'80 م/ث²',
|
||||
'0.2 م/ث²',
|
||||
'16 م/ث²',
|
||||
],
|
||||
correctIndex: 0,
|
||||
explanation:
|
||||
'تطبيق مباشر لقانون نيوتن الثاني: a = F / m = 20 / 4 = 5 م/ث².',
|
||||
),
|
||||
RemedialQuestion(
|
||||
id: 3,
|
||||
question:
|
||||
'ما الفرق بين الكمية القياسية والكمية المتجهة في التعبير الفيزيائي الدقيق؟',
|
||||
options: [
|
||||
'الكمية القياسية دائماً موجبة والمتجهة دائماً سالبة',
|
||||
'الكمية القياسية تُحدد بالمقدار والوحدة فقط، بينما المتجهة تتطلب مقداراً ووحدة واتجاهاً محدداً',
|
||||
'لا يوجد فرق، كلاهما يُقاس بنفس الطريقة',
|
||||
'الكمية المتجهة تُقاس في الفضاء فقط',
|
||||
],
|
||||
correctIndex: 1,
|
||||
explanation:
|
||||
'الكمية القياسية مثل الكتلة والزمن، بينما المتجهة مثل القوة والسرعة المتجهة تتطلب تحديد الاتجاه بدقة.',
|
||||
),
|
||||
];
|
||||
Future<List<RemedialQuestion>> 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<String, dynamic>.from(decoded['data'] as Map? ?? const {});
|
||||
return (data['questions'] as List? ?? const [])
|
||||
.map((item) => RemedialQuestion.fromJson(Map<String, dynamic>.from(item as Map)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<bool> 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';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user