Harden published curriculum and student flows
This commit is contained in:
@@ -55,6 +55,7 @@ class ErrorNotebookItem {
|
||||
final String errorCategory; // 'conceptual', 'calculation', 'rushed', 'misinterpretation'
|
||||
final String status; // 'pending_remediation', 'in_remediation', 'mastered'
|
||||
final int remediationAttemptsCount;
|
||||
final bool retentionIsDue;
|
||||
final String? masteredAt;
|
||||
final String createdAt;
|
||||
|
||||
@@ -72,6 +73,7 @@ class ErrorNotebookItem {
|
||||
required this.errorCategory,
|
||||
required this.status,
|
||||
required this.remediationAttemptsCount,
|
||||
this.retentionIsDue = false,
|
||||
this.masteredAt,
|
||||
required this.createdAt,
|
||||
});
|
||||
@@ -125,6 +127,7 @@ class ErrorNotebookItem {
|
||||
errorCategory: json['error_category']?.toString() ?? 'conceptual',
|
||||
status: json['status']?.toString() ?? 'pending_remediation',
|
||||
remediationAttemptsCount: (json['remediation_attempts_count'] as num?)?.toInt() ?? 0,
|
||||
retentionIsDue: json['retention_is_due'] == true || json['retention_is_due'] == 1 || json['retention_is_due'] == '1',
|
||||
masteredAt: json['mastered_at']?.toString(),
|
||||
createdAt: json['created_at']?.toString() ?? '',
|
||||
);
|
||||
@@ -149,6 +152,7 @@ class ErrorNotebookItem {
|
||||
errorCategory: errorCategory,
|
||||
status: status ?? this.status,
|
||||
remediationAttemptsCount: remediationAttemptsCount ?? this.remediationAttemptsCount,
|
||||
retentionIsDue: retentionIsDue,
|
||||
masteredAt: masteredAt ?? this.masteredAt,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
@@ -158,16 +162,12 @@ class ErrorNotebookItem {
|
||||
class RemedialQuestion {
|
||||
final int id;
|
||||
final String question;
|
||||
final List<String> options;
|
||||
final int correctIndex;
|
||||
final String explanation;
|
||||
final List<RemedialOption> options;
|
||||
|
||||
RemedialQuestion({
|
||||
required this.id,
|
||||
required this.question,
|
||||
required this.options,
|
||||
required this.correctIndex,
|
||||
required this.explanation,
|
||||
});
|
||||
|
||||
factory RemedialQuestion.fromJson(Map<String, dynamic> json) {
|
||||
@@ -175,9 +175,17 @@ class RemedialQuestion {
|
||||
return RemedialQuestion(
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
question: json['question']?.toString() ?? '',
|
||||
options: rawOptions.map((e) => e.toString()).toList(),
|
||||
correctIndex: (json['correct_index'] as num?)?.toInt() ?? 0,
|
||||
explanation: json['explanation']?.toString() ?? '',
|
||||
options: rawOptions.map((e) => RemedialOption.fromJson(Map<String, dynamic>.from(e as Map))).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RemedialOption {
|
||||
final int id;
|
||||
final String text;
|
||||
RemedialOption({required this.id, required this.text});
|
||||
factory RemedialOption.fromJson(Map<String, dynamic> json) => RemedialOption(
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
text: json['text']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,6 +209,8 @@ class CurriculumLessonItemModel {
|
||||
final List<String> outcomes;
|
||||
final String? markdownFilePath;
|
||||
final String? curriculumLessonId;
|
||||
final String? curriculumVersion;
|
||||
final String? primaryLessonAssetId;
|
||||
final int durationSeconds;
|
||||
final int checkpointsCount;
|
||||
final bool isCompleted;
|
||||
@@ -220,6 +222,8 @@ class CurriculumLessonItemModel {
|
||||
this.outcomes = const [],
|
||||
this.markdownFilePath,
|
||||
this.curriculumLessonId,
|
||||
this.curriculumVersion,
|
||||
this.primaryLessonAssetId,
|
||||
this.durationSeconds = 0,
|
||||
this.checkpointsCount = 0,
|
||||
this.isCompleted = false,
|
||||
@@ -254,6 +258,8 @@ class CurriculumLessonItemModel {
|
||||
outcomes: outs,
|
||||
markdownFilePath: filePath,
|
||||
curriculumLessonId: json['curriculum_lesson_id']?.toString(),
|
||||
curriculumVersion: json['curriculum_version']?.toString(),
|
||||
primaryLessonAssetId: json['primary_lesson_asset_id']?.toString(),
|
||||
durationSeconds: (json['duration_seconds'] as num?)?.toInt() ?? 0,
|
||||
checkpointsCount: (json['checkpoints_count'] as num?)?.toInt() ?? 0,
|
||||
isCompleted: (json['is_completed'] as bool?) ?? false,
|
||||
|
||||
@@ -28,19 +28,53 @@ class GradeLevelModel {
|
||||
GradeLevelModel(key: 'grade_9', name: 'الصف التاسع الأساسي', number: 9),
|
||||
GradeLevelModel(key: 'grade_10', name: 'الصف العاشر الأساسي', number: 10),
|
||||
GradeLevelModel(key: 'grade_11', name: 'الأول ثانوي', number: 11),
|
||||
GradeLevelModel(key: 'tawjihi_2008', name: 'الثاني ثانوي — توجيهي 2008', number: 12),
|
||||
GradeLevelModel(
|
||||
key: 'tawjihi_2008', name: 'الثاني ثانوي — توجيهي 2008', number: 12),
|
||||
];
|
||||
|
||||
static String normalizeGradeKey(String raw) {
|
||||
final clean = raw.toLowerCase().trim();
|
||||
if (clean.contains('عاشر') || clean.contains('grade_10') || clean.contains('grade 10') || clean.contains('grade10') || clean == '10') return 'grade_10';
|
||||
if (clean.contains('تاسع') || clean.contains('grade_9') || clean.contains('grade 9') || clean.contains('grade9') || clean == '9') return 'grade_9';
|
||||
if (clean.contains('ثامن') || clean.contains('grade_8') || clean.contains('grade 8') || clean.contains('grade8') || clean == '8') return 'grade_8';
|
||||
if (clean.contains('سابع') || clean.contains('grade_7') || clean.contains('grade 7') || clean.contains('grade7') || clean == '7') return 'grade_7';
|
||||
if (clean.contains('سادس') || clean.contains('grade_6') || clean.contains('grade 6') || clean.contains('grade6') || clean == '6') return 'grade_6';
|
||||
if (clean.contains('خامس') || clean.contains('grade_5') || clean.contains('grade 5') || clean.contains('grade5') || clean == '5') return 'grade_5';
|
||||
if (clean.contains('حادي') || clean.contains('أول ثانوي') || clean.contains('grade_11') || clean.contains('grade 11') || clean == '11') return 'grade_11';
|
||||
if (clean.contains('ثاني عشر') || clean.contains('توجيهي') || clean.contains('tawjihi') || clean.contains('grade_12') || clean.contains('grade 12') || clean == '12') return 'tawjihi_2008';
|
||||
if (clean.contains('عاشر') ||
|
||||
clean.contains('grade_10') ||
|
||||
clean.contains('grade 10') ||
|
||||
clean.contains('grade10') ||
|
||||
clean == '10') return 'grade_10';
|
||||
if (clean.contains('تاسع') ||
|
||||
clean.contains('grade_9') ||
|
||||
clean.contains('grade 9') ||
|
||||
clean.contains('grade9') ||
|
||||
clean == '9') return 'grade_9';
|
||||
if (clean.contains('ثامن') ||
|
||||
clean.contains('grade_8') ||
|
||||
clean.contains('grade 8') ||
|
||||
clean.contains('grade8') ||
|
||||
clean == '8') return 'grade_8';
|
||||
if (clean.contains('سابع') ||
|
||||
clean.contains('grade_7') ||
|
||||
clean.contains('grade 7') ||
|
||||
clean.contains('grade7') ||
|
||||
clean == '7') return 'grade_7';
|
||||
if (clean.contains('سادس') ||
|
||||
clean.contains('grade_6') ||
|
||||
clean.contains('grade 6') ||
|
||||
clean.contains('grade6') ||
|
||||
clean == '6') return 'grade_6';
|
||||
if (clean.contains('خامس') ||
|
||||
clean.contains('grade_5') ||
|
||||
clean.contains('grade 5') ||
|
||||
clean.contains('grade5') ||
|
||||
clean == '5') return 'grade_5';
|
||||
if (clean.contains('حادي') ||
|
||||
clean.contains('أول ثانوي') ||
|
||||
clean.contains('grade_11') ||
|
||||
clean.contains('grade 11') ||
|
||||
clean == '11') return 'grade_11';
|
||||
if (clean.contains('ثاني عشر') ||
|
||||
clean.contains('توجيهي') ||
|
||||
clean.contains('tawjihi') ||
|
||||
clean.contains('grade_12') ||
|
||||
clean.contains('grade 12') ||
|
||||
clean == '12') return 'tawjihi_2008';
|
||||
return raw;
|
||||
}
|
||||
|
||||
@@ -54,8 +88,7 @@ class GradeLevelModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 100% Live Dynamic Curriculum Repository (Zero Hardcoded Data)
|
||||
/// Dynamically fetches all grades (الصف الأول إلى التوجيهي) and their subjects from Live API.
|
||||
/// Reads only the server-published curriculum catalogue.
|
||||
class CurriculumRepository {
|
||||
final ApiClient _api;
|
||||
|
||||
@@ -63,11 +96,12 @@ class CurriculumRepository {
|
||||
|
||||
/// Fetch full curriculum tree from server
|
||||
Future<Map<String, dynamic>> getRawCurriculumTree() async {
|
||||
final res = await _api.get(AppConfig.curriculumTreeEndpoint, requiresAuth: false);
|
||||
if (res is Map && res['data'] != null && res['data'] is Map) {
|
||||
final res =
|
||||
await _api.get(AppConfig.curriculumTreeEndpoint, requiresAuth: false);
|
||||
if (res is Map && res['status'] == 'success' && res['data'] is Map) {
|
||||
return Map<String, dynamic>.from(res['data']);
|
||||
}
|
||||
return {};
|
||||
throw ApiException('استجابة فهرس الدروس المنشورة غير صالحة.');
|
||||
}
|
||||
|
||||
/// Fetch available grades from Live Server Manifest
|
||||
@@ -78,22 +112,28 @@ class CurriculumRepository {
|
||||
for (var grade in GradeLevelModel.allK12Grades) {
|
||||
final norm = GradeLevelModel.normalizeGradeKey(grade.key);
|
||||
final shortKey = norm.replaceAll('grade_', '');
|
||||
if (tree.containsKey(grade.key) || tree.containsKey(norm) || tree.containsKey(shortKey)) {
|
||||
if (tree.containsKey(grade.key) ||
|
||||
tree.containsKey(norm) ||
|
||||
tree.containsKey(shortKey)) {
|
||||
available.add(grade);
|
||||
}
|
||||
}
|
||||
|
||||
return available.isNotEmpty ? available : GradeLevelModel.allK12Grades;
|
||||
return available;
|
||||
}
|
||||
|
||||
/// Fetch subjects for a specific grade level dynamically from Live Backend
|
||||
Future<List<SubjectModel>> getSubjects({String gradeLevel = 'grade_10', String stream = 'scientific'}) async {
|
||||
Future<List<SubjectModel>> getSubjects(
|
||||
{String gradeLevel = 'grade_10', String stream = 'scientific'}) async {
|
||||
final normGrade = GradeLevelModel.normalizeGradeKey(gradeLevel);
|
||||
AppLogger.log('Fetching live curriculum tree from /api/curriculum/tree (Grade: $gradeLevel -> $normGrade)...', tag: 'CURRICULUM_REPO');
|
||||
|
||||
AppLogger.log(
|
||||
'Fetching live curriculum tree from /api/curriculum/tree (Grade: $gradeLevel -> $normGrade)...',
|
||||
tag: 'CURRICULUM_REPO');
|
||||
|
||||
final tree = await getRawCurriculumTree();
|
||||
if (tree.isEmpty) {
|
||||
AppLogger.log('No curriculum data returned from server.', tag: 'CURRICULUM_REPO');
|
||||
AppLogger.log('No curriculum data returned from server.',
|
||||
tag: 'CURRICULUM_REPO');
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -119,7 +159,8 @@ class CurriculumRepository {
|
||||
}
|
||||
if (entry.value is Map && entry.value['name'] != null) {
|
||||
final name = entry.value['name'].toString();
|
||||
if (GradeLevelModel.normalizeGradeKey(name) == normGrade || name.contains(normGrade)) {
|
||||
if (GradeLevelModel.normalizeGradeKey(name) == normGrade ||
|
||||
name.contains(normGrade)) {
|
||||
gradeData = Map<String, dynamic>.from(entry.value);
|
||||
break;
|
||||
}
|
||||
@@ -128,8 +169,12 @@ class CurriculumRepository {
|
||||
}
|
||||
}
|
||||
|
||||
if (gradeData == null || gradeData['subjects'] == null || gradeData['subjects'] is! Map) {
|
||||
AppLogger.log('No subjects found in grade $gradeLevel ($normGrade) on server.', tag: 'CURRICULUM_REPO');
|
||||
if (gradeData == null ||
|
||||
gradeData['subjects'] == null ||
|
||||
gradeData['subjects'] is! Map) {
|
||||
AppLogger.log(
|
||||
'No subjects found in grade $gradeLevel ($normGrade) on server.',
|
||||
tag: 'CURRICULUM_REPO');
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -143,74 +188,130 @@ class CurriculumRepository {
|
||||
}
|
||||
});
|
||||
|
||||
AppLogger.log('Successfully parsed ${liveSubjects.length} live subjects for $gradeLevel: ${liveSubjects.map((s) => s.title).join(', ')}', tag: 'CURRICULUM_REPO');
|
||||
AppLogger.log(
|
||||
'Successfully parsed ${liveSubjects.length} live subjects for $gradeLevel: ${liveSubjects.map((s) => s.title).join(', ')}',
|
||||
tag: 'CURRICULUM_REPO');
|
||||
return liveSubjects;
|
||||
}
|
||||
|
||||
Future<List<PublishedLessonVideoModel>> getPublishedLessonVideos(String curriculumLessonId) async {
|
||||
AppLogger.event('FetchPublishedVideosRequested', details: {'curriculumLessonId': curriculumLessonId}, tag: 'CURRICULUM_REPO');
|
||||
final res = await _api.get('/api/curriculum/lessons/$curriculumLessonId/videos');
|
||||
final data = res is Map ? res['data'] : null;
|
||||
final raw = data is Map ? data['items'] : null;
|
||||
if (raw is! List) return const [];
|
||||
final list = raw.whereType<Map>().map((item) => PublishedLessonVideoModel.fromJson(Map<String, dynamic>.from(item))).where((item) => item.videoVersionId.isNotEmpty).toList();
|
||||
AppLogger.event('FetchPublishedVideosSuccess', details: {'count': list.length, 'teachers': list.map((v) => v.teacherName).toList()}, tag: 'CURRICULUM_REPO');
|
||||
return list;
|
||||
Future<List<PublishedLessonVideoModel>> getPublishedLessonVideos(
|
||||
String curriculumLessonId) async {
|
||||
return (await getPublishedLessonVideosPage(curriculumLessonId)).items;
|
||||
}
|
||||
|
||||
Future<LessonPlaybackData> getVideoVersionPlayback(String videoVersionId) async {
|
||||
AppLogger.event('FetchVideoPlaybackRequested', details: {'videoVersionId': videoVersionId}, tag: 'CURRICULUM_REPO');
|
||||
Future<PublishedLessonVideoPage> getPublishedLessonVideosPage(
|
||||
String curriculumLessonId,
|
||||
{int cursor = 0,
|
||||
int limit = 20}) async {
|
||||
AppLogger.event('FetchPublishedVideosRequested',
|
||||
details: {'curriculumLessonId': curriculumLessonId},
|
||||
tag: 'CURRICULUM_REPO');
|
||||
final res = await _api.get(
|
||||
'/api/curriculum/lessons/$curriculumLessonId/videos',
|
||||
queryParams: {'cursor': cursor, 'limit': limit});
|
||||
final data = res is Map ? res['data'] : null;
|
||||
final raw = data is Map ? data['items'] : null;
|
||||
if (raw is! List || data is! Map)
|
||||
throw ApiException('استجابة قائمة الحصص غير صالحة.');
|
||||
final list = raw
|
||||
.whereType<Map>()
|
||||
.map((item) =>
|
||||
PublishedLessonVideoModel.fromJson(Map<String, dynamic>.from(item)))
|
||||
.where((item) => item.videoVersionId.isNotEmpty)
|
||||
.toList();
|
||||
AppLogger.event('FetchPublishedVideosSuccess',
|
||||
details: {
|
||||
'count': list.length,
|
||||
'teachers': list.map((v) => v.teacherName).toList()
|
||||
},
|
||||
tag: 'CURRICULUM_REPO');
|
||||
return PublishedLessonVideoPage(
|
||||
items: list,
|
||||
availableCount: (data['available_count'] as num?)?.toInt() ?? list.length,
|
||||
nextCursor: (data['next_cursor'] as num?)?.toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<LessonPlaybackData> getVideoVersionPlayback(
|
||||
String videoVersionId) async {
|
||||
AppLogger.event('FetchVideoPlaybackRequested',
|
||||
details: {'videoVersionId': videoVersionId}, tag: 'CURRICULUM_REPO');
|
||||
final res = await _api.get('/api/video-versions/$videoVersionId/playback');
|
||||
if (res is Map && res['data'] is Map) {
|
||||
final pb = LessonPlaybackData.fromJson(Map<String, dynamic>.from(res['data']));
|
||||
AppLogger.event('FetchVideoPlaybackSuccess', details: {
|
||||
'videoVersionId': pb.videoVersionId,
|
||||
'videoUrl': pb.videoUrl,
|
||||
'duration': pb.durationSeconds,
|
||||
}, tag: 'CURRICULUM_REPO');
|
||||
final pb =
|
||||
LessonPlaybackData.fromJson(Map<String, dynamic>.from(res['data']));
|
||||
AppLogger.event('FetchVideoPlaybackSuccess',
|
||||
details: {
|
||||
'videoVersionId': pb.videoVersionId,
|
||||
'duration': pb.durationSeconds,
|
||||
},
|
||||
tag: 'CURRICULUM_REPO');
|
||||
return pb;
|
||||
}
|
||||
throw ApiException('فشل جلب تشغيل الحصة المنشورة من الخادم');
|
||||
}
|
||||
|
||||
Future<String> startWatchSession(String videoVersionId) async {
|
||||
AppLogger.event('StartWatchSessionRequested', details: {'videoVersionId': videoVersionId}, tag: 'CURRICULUM_REPO');
|
||||
final res = await _api.post('/api/video-versions/$videoVersionId/watch-sessions', body: const {});
|
||||
AppLogger.event('StartWatchSessionRequested',
|
||||
details: {'videoVersionId': videoVersionId}, tag: 'CURRICULUM_REPO');
|
||||
final res = await _api.post(
|
||||
'/api/video-versions/$videoVersionId/watch-sessions',
|
||||
body: const {});
|
||||
final data = res is Map ? res['data'] : null;
|
||||
final source = data is Map ? data : res;
|
||||
final id = source is Map ? source['watch_session_id']?.toString() : null;
|
||||
if (id == null || id.isEmpty) throw ApiException('لم ينشئ الخادم جلسة مشاهدة.');
|
||||
AppLogger.event('StartWatchSessionSuccess', details: {'sessionId': id}, tag: 'CURRICULUM_REPO');
|
||||
if (id == null || id.isEmpty)
|
||||
throw ApiException('لم ينشئ الخادم جلسة مشاهدة.');
|
||||
AppLogger.event('StartWatchSessionSuccess',
|
||||
details: {'sessionId': id}, tag: 'CURRICULUM_REPO');
|
||||
return id;
|
||||
}
|
||||
Future<void> recordWatchEvent(String sessionId, int sequenceNo, String eventType, int positionMs) async {
|
||||
AppLogger.event('RecordWatchEvent', details: {
|
||||
'sessionId': sessionId,
|
||||
'seq': sequenceNo,
|
||||
'type': eventType,
|
||||
'positionMs': positionMs,
|
||||
}, tag: 'CURRICULUM_REPO');
|
||||
await _api.post('/api/watch-sessions/$sessionId/events', body: {'sequence_no': sequenceNo, 'event_type': eventType, 'position_ms': positionMs});
|
||||
|
||||
Future<void> recordWatchEvent(String sessionId, int sequenceNo,
|
||||
String eventType, int positionMs) async {
|
||||
AppLogger.event('RecordWatchEvent',
|
||||
details: {
|
||||
'sessionId': sessionId,
|
||||
'seq': sequenceNo,
|
||||
'type': eventType,
|
||||
'positionMs': positionMs,
|
||||
},
|
||||
tag: 'CURRICULUM_REPO');
|
||||
await _api.post('/api/watch-sessions/$sessionId/events', body: {
|
||||
'sequence_no': sequenceNo,
|
||||
'event_type': eventType,
|
||||
'position_ms': positionMs
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> saveProgress({required int lessonId, required int positionSeconds, required int watchedSeconds}) async {
|
||||
AppLogger.event('SaveProgressRequested', details: {
|
||||
'lessonId': lessonId,
|
||||
'position': positionSeconds,
|
||||
'watched': watchedSeconds,
|
||||
}, tag: 'CURRICULUM_REPO');
|
||||
Future<void> saveProgress(
|
||||
{required int lessonId,
|
||||
required int positionSeconds,
|
||||
required int watchedSeconds}) async {
|
||||
AppLogger.event('SaveProgressRequested',
|
||||
details: {
|
||||
'lessonId': lessonId,
|
||||
'position': positionSeconds,
|
||||
'watched': watchedSeconds,
|
||||
},
|
||||
tag: 'CURRICULUM_REPO');
|
||||
await _api.post('/api/student/lessons/$lessonId/progress', body: {
|
||||
'position_seconds': positionSeconds,
|
||||
'watched_seconds': watchedSeconds,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> submitCheckpoint({required int examId, required int questionId, required int optionId}) async {
|
||||
AppLogger.event('SubmitCheckpointRequested', details: {
|
||||
'examId': examId,
|
||||
'questionId': questionId,
|
||||
'optionId': optionId,
|
||||
}, tag: 'CURRICULUM_REPO');
|
||||
Future<void> submitCheckpoint(
|
||||
{required int examId,
|
||||
required int questionId,
|
||||
required int optionId}) async {
|
||||
AppLogger.event('SubmitCheckpointRequested',
|
||||
details: {
|
||||
'examId': examId,
|
||||
'questionId': questionId,
|
||||
'optionId': optionId,
|
||||
},
|
||||
tag: 'CURRICULUM_REPO');
|
||||
await _api.post('/api/exams/$examId/submit', body: {
|
||||
'answers': [
|
||||
{'question_id': questionId, 'selected_option_id': optionId}
|
||||
@@ -226,8 +327,36 @@ class PublishedLessonVideoModel {
|
||||
final double rating;
|
||||
final int ratingCount;
|
||||
final bool isNew;
|
||||
const PublishedLessonVideoModel({required this.videoVersionId, required this.submissionId, required this.teacherName, required this.rating, required this.ratingCount, required this.isNew});
|
||||
factory PublishedLessonVideoModel.fromJson(Map<String, dynamic> json) => PublishedLessonVideoModel(
|
||||
videoVersionId: json['video_version_id']?.toString() ?? '', submissionId: json['submission_id']?.toString() ?? '', teacherName: json['teacher_name']?.toString() ?? 'معلم معتمد', rating: (json['rating'] as num?)?.toDouble() ?? 0, ratingCount: (json['rating_count'] as num?)?.toInt() ?? 0, isNew: json['is_new'] == true,
|
||||
);
|
||||
final int durationSeconds;
|
||||
final DateTime? publishedAt;
|
||||
const PublishedLessonVideoModel(
|
||||
{required this.videoVersionId,
|
||||
required this.submissionId,
|
||||
required this.teacherName,
|
||||
required this.rating,
|
||||
required this.ratingCount,
|
||||
required this.isNew,
|
||||
required this.durationSeconds,
|
||||
this.publishedAt});
|
||||
factory PublishedLessonVideoModel.fromJson(Map<String, dynamic> json) =>
|
||||
PublishedLessonVideoModel(
|
||||
videoVersionId: json['video_version_id']?.toString() ?? '',
|
||||
submissionId: json['submission_id']?.toString() ?? '',
|
||||
teacherName: json['teacher_name']?.toString() ?? '',
|
||||
rating: (json['rating'] as num?)?.toDouble() ?? 0,
|
||||
ratingCount: (json['rating_count'] as num?)?.toInt() ?? 0,
|
||||
isNew: json['is_new'] == true,
|
||||
durationSeconds: (json['duration_seconds'] as num?)?.toInt() ?? 0,
|
||||
publishedAt: DateTime.tryParse(json['published_at']?.toString() ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
class PublishedLessonVideoPage {
|
||||
final List<PublishedLessonVideoModel> items;
|
||||
final int availableCount;
|
||||
final int? nextCursor;
|
||||
const PublishedLessonVideoPage(
|
||||
{required this.items,
|
||||
required this.availableCount,
|
||||
required this.nextCursor});
|
||||
}
|
||||
|
||||
@@ -23,18 +23,7 @@ class ErrorNotebookRepository {
|
||||
'summary': ErrorNotebookSummary.fromJson(Map<String, dynamic>.from(data['summary'] as Map? ?? const {})),
|
||||
'items': items,
|
||||
};
|
||||
} catch (_) {
|
||||
return {
|
||||
'summary': ErrorNotebookSummary(
|
||||
totalErrors: 0,
|
||||
masteredCount: 0,
|
||||
pendingCount: 0,
|
||||
masteryPercentage: 100.0,
|
||||
bySubject: {},
|
||||
),
|
||||
'items': <ErrorNotebookItem>[],
|
||||
};
|
||||
}
|
||||
} catch (_) { rethrow; }
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getChildErrorNotebook(int studentId) async {
|
||||
@@ -48,18 +37,7 @@ class ErrorNotebookRepository {
|
||||
'summary': ErrorNotebookSummary.fromJson(Map<String, dynamic>.from(data['summary'] as Map? ?? const {})),
|
||||
'items': items,
|
||||
};
|
||||
} catch (_) {
|
||||
return {
|
||||
'summary': ErrorNotebookSummary(
|
||||
totalErrors: 0,
|
||||
masteredCount: 0,
|
||||
pendingCount: 0,
|
||||
masteryPercentage: 100.0,
|
||||
bySubject: {},
|
||||
),
|
||||
'items': <ErrorNotebookItem>[],
|
||||
};
|
||||
}
|
||||
} catch (_) { rethrow; }
|
||||
}
|
||||
|
||||
Future<String?> logError({
|
||||
@@ -99,30 +77,33 @@ class ErrorNotebookRepository {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<RemedialQuestion>> getRemediationQuiz({required String errorUuid, required String topicName}) async {
|
||||
Future<Map<String, dynamic>> getRemediationQuiz({required String errorUuid}) async {
|
||||
try {
|
||||
final decoded = await _api.get(
|
||||
final decoded = await _api.post(
|
||||
'/api/student/error-notebook/remediation-quiz',
|
||||
queryParams: {'error_uuid': errorUuid, 'topic_name': topicName},
|
||||
body: {'error_uuid': errorUuid},
|
||||
);
|
||||
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();
|
||||
return {
|
||||
'attempt_uuid': data['attempt_uuid']?.toString() ?? '',
|
||||
'questions': (data['questions'] as List? ?? const [])
|
||||
.map((item) => RemedialQuestion.fromJson(Map<String, dynamic>.from(item as Map)))
|
||||
.toList(),
|
||||
};
|
||||
} catch (_) {
|
||||
return [];
|
||||
return {'attempt_uuid': '', 'questions': <RemedialQuestion>[]};
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> resolveError(String errorUuid) async {
|
||||
Future<Map<String, dynamic>?> resolveError(String errorUuid, String attemptUuid, List<int> answers) async {
|
||||
try {
|
||||
final decoded = await _api.post(
|
||||
'/api/student/error-notebook/resolve',
|
||||
body: {'error_uuid': errorUuid},
|
||||
body: {'error_uuid': errorUuid, 'attempt_uuid': attemptUuid, 'answers': answers},
|
||||
);
|
||||
return decoded is Map && decoded['status'] == 'success';
|
||||
return decoded is Map && decoded['status'] == 'success' ? Map<String, dynamic>.from(decoded) : null;
|
||||
} catch (_) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user