Fix Flutter exam parsing, make exams tab dynamic from API, and add intelligent exam resolution to submitExam

This commit is contained in:
Hamza-Ayed
2026-09-03 16:59:14 +03:00
parent 0a61e32033
commit 445a1aa0e3
3 changed files with 206 additions and 100 deletions
@@ -36,21 +36,33 @@ class ExamModel {
}
return ExamModel(
id: (json['id'] as num?)?.toInt() ?? 0,
id: _toInt(json['id']),
uuid: json['uuid']?.toString() ?? '',
courseId: (json['course_id'] as num?)?.toInt() ?? 0,
lessonId: (json['lesson_id'] as num?)?.toInt(),
courseId: _toInt(json['course_id']),
lessonId: json['lesson_id'] != null ? _toInt(json['lesson_id']) : null,
title: json['title']?.toString() ?? 'امتحان تشخيصي',
description: json['description']?.toString() ?? '',
scope: json['scope']?.toString() ?? 'general',
passingPercentage: (json['passing_percentage'] as num?)?.toDouble() ?? 60.0,
durationMinutes: (json['duration_minutes'] as num?)?.toInt() ?? 20,
questionsCount: (json['questions_count'] as num?)?.toInt() ?? parsedQuestions.length,
scope: json['scope']?.toString() ?? 'unit_exam',
passingPercentage: _toDouble(json['passing_percentage'], 60.0),
durationMinutes: _toInt(json['duration_minutes'], 20),
questionsCount: _toInt(json['questions_count'], parsedQuestions.length),
questions: parsedQuestions,
);
}
}
int _toInt(dynamic value, [int defaultValue = 0]) {
if (value == null) return defaultValue;
if (value is num) return value.toInt();
return int.tryParse(value.toString()) ?? defaultValue;
}
double _toDouble(dynamic value, [double defaultValue = 0.0]) {
if (value == null) return defaultValue;
if (value is num) return value.toDouble();
return double.tryParse(value.toString()) ?? defaultValue;
}
class QuestionModel {
final int id;
final String questionText;
@@ -82,10 +94,10 @@ class QuestionModel {
}
return QuestionModel(
id: (json['id'] as num?)?.toInt() ?? 0,
id: _toInt(json['id']),
questionText: json['question_text']?.toString() ?? '',
questionType: json['question_type']?.toString() ?? 'multiple_choice',
points: (json['points'] as num?)?.toInt() ?? 10,
points: _toInt(json['points'], 10),
topicTag: json['topic_tag']?.toString() ?? 'المفاهيم العامة',
explanationText: json['explanation_text']?.toString(),
aiHint: json['ai_hint']?.toString(),
@@ -109,10 +121,10 @@ class QuestionOptionModel {
factory QuestionOptionModel.fromJson(Map<String, dynamic> json) {
return QuestionOptionModel(
id: (json['id'] as num?)?.toInt() ?? 0,
id: _toInt(json['id']),
optionText: json['option_text']?.toString() ?? '',
isCorrect: json['is_correct'] == 1 || json['is_correct'] == true,
sequenceOrder: (json['sequence_order'] as num?)?.toInt() ?? 1,
isCorrect: json['is_correct'] == 1 || json['is_correct'] == true || json['is_correct'] == '1',
sequenceOrder: _toInt(json['sequence_order'], 1),
);
}
}
@@ -158,15 +170,15 @@ class ExamSubmissionResultModel {
}
return ExamSubmissionResultModel(
attemptId: (json['attempt_id'] as num?)?.toInt() ?? 0,
score: (json['score'] as num?)?.toInt() ?? 0,
totalScore: (json['total_score'] as num?)?.toInt() ?? 0,
percentage: (json['percentage'] as num?)?.toDouble() ?? 0.0,
passed: json['passed'] == true || json['passed'] == 1,
rewindSeconds: (json['rewind_seconds'] as num?)?.toInt() ?? 0,
attemptId: _toInt(json['attempt_id']),
score: _toInt(json['score']),
totalScore: _toInt(json['total_score'], 100),
percentage: _toDouble(json['percentage']),
passed: json['passed'] == true || json['passed'] == 1 || json['passed'] == '1',
rewindSeconds: _toInt(json['rewind_seconds']),
aiDiagnosticReport: json['ai_diagnostic_report']?.toString() ?? '',
weakTopics: parsedTopics,
tawjihiReadinessScore: (json['tawjihi_readiness_score'] as num?)?.toDouble() ?? 0.0,
tawjihiReadinessScore: _toDouble(json['tawjihi_readiness_score'], 50.0),
detailedAnswers: parsedDetails,
);
}
@@ -191,10 +203,10 @@ class DetailedAnswerModel {
factory DetailedAnswerModel.fromJson(Map<String, dynamic> json) {
return DetailedAnswerModel(
questionId: (json['question_id'] as num?)?.toInt() ?? 0,
selectedOptionId: (json['selected_option_id'] as num?)?.toInt() ?? 0,
isCorrect: json['is_correct'] == 1 || json['is_correct'] == true,
pointsAwarded: (json['points_awarded'] as num?)?.toInt() ?? 0,
questionId: _toInt(json['question_id']),
selectedOptionId: _toInt(json['selected_option_id']),
isCorrect: json['is_correct'] == 1 || json['is_correct'] == true || json['is_correct'] == '1',
pointsAwarded: _toInt(json['points_awarded']),
explanation: json['explanation']?.toString(),
aiHint: json['ai_hint']?.toString(),
);
@@ -21,6 +21,8 @@ import '../../widgets/luxury_widgets.dart';
import '../player/socratic_video_player_screen.dart';
import '../exams/adaptive_exam_screen.dart';
import 'curriculum_document_viewer_screen.dart';
import '../../../data/models/exam_model.dart';
import '../../../data/repositories/app_repositories.dart';
/// الشاشة المركزية للمادة الدراسية وبوابات الدروس والامتحانات والمصادر
class SubjectHubScreen extends StatefulWidget {
@@ -34,11 +36,13 @@ class SubjectHubScreen extends StatefulWidget {
class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
late Future<List<ExamModel>> _examsFuture;
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
_examsFuture = ExamRepository().getExams(courseId: 1, scope: 'unit_exam');
}
@override
@@ -272,88 +276,128 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
/// Tab 3: Question Bank & Adaptive Unit Exams
Widget _buildExamsTab(BuildContext context) {
final exams = [
const SubjectExamModel(id: 'ex1', title: 'اختبار الفهم الشامل: الوحدة الأولى (أنظمة المعادلات)', questionsCount: 15, durationMinutes: 45, targetScore: 100),
const SubjectExamModel(id: 'ex2', title: 'نماذج أسئلة الوزارة للسنوات السابقة', questionsCount: 15, durationMinutes: 45, targetScore: 100),
const SubjectExamModel(id: 'ex3', title: 'اختبار تشخيص الثغرات التكيفي بالذكاء الاصطناعي', questionsCount: 15, durationMinutes: 30, targetScore: 100),
];
return FutureBuilder<List<ExamModel>>(
future: _examsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CupertinoActivityIndicator(color: AppColors.saqelCyan),
);
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
itemCount: exams.length,
itemBuilder: (context, idx) {
final exam = exams[idx];
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: LuxuryCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
exam.title,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColors.emeraldGreen.withAlpha(25),
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'جاهز للتقديم ✍️',
style: TextStyle(color: AppColors.emeraldGreen, fontSize: 11, fontWeight: FontWeight.w700),
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(CupertinoIcons.question_circle, color: AppColors.textSecondaryDark, size: 14),
const SizedBox(width: 4),
Text('${exam.questionsCount} أسئلة', style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12)),
const SizedBox(width: 12),
const Icon(CupertinoIcons.time, color: AppColors.textSecondaryDark, size: 14),
const SizedBox(width: 4),
Text('${exam.durationMinutes} د', style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12)),
],
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.appleBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: () {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (context) => AdaptiveExamScreen(
examId: idx + 1,
title: exam.title,
subjectTitle: widget.subject.title,
),
),
);
},
child: const Text('بدء الاختبار 🚀', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700)),
),
],
),
],
),
),
final liveExams = snapshot.data ?? [];
if (liveExams.isEmpty) {
// Default to Primary Unit Exam if none fetched yet
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
children: [
_buildExamCard(
context,
examId: 1,
title: 'اختبار الفهم الشامل: الوحدة الأولى (أنظمة المعادلات)',
questionsCount: 50,
durationMinutes: 45,
),
],
);
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 20),
itemCount: liveExams.length,
itemBuilder: (context, idx) {
final exam = liveExams[idx];
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildExamCard(
context,
examId: exam.id > 0 ? exam.id : 1,
title: exam.title,
questionsCount: exam.questionsCount > 0 ? exam.questionsCount : 50,
durationMinutes: exam.durationMinutes > 0 ? exam.durationMinutes : 45,
),
);
},
);
},
);
}
Widget _buildExamCard(
BuildContext context, {
required int examId,
required String title,
required int questionsCount,
required int durationMinutes,
}) {
return LuxuryCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
title,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColors.emeraldGreen.withAlpha(25),
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'جاهز للتقديم ✍️',
style: TextStyle(color: AppColors.emeraldGreen, fontSize: 11, fontWeight: FontWeight.w700),
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(CupertinoIcons.question_circle, color: AppColors.textSecondaryDark, size: 14),
const SizedBox(width: 4),
Text('$questionsCount سؤالاً في البنك', style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12)),
const SizedBox(width: 12),
const Icon(CupertinoIcons.time, color: AppColors.textSecondaryDark, size: 14),
const SizedBox(width: 4),
Text('$durationMinutes د', style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 12)),
],
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.appleBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: () {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (context) => AdaptiveExamScreen(
examId: examId,
title: title,
subjectTitle: widget.subject.title,
),
),
);
},
child: const Text('بدء الاختبار 🚀', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700)),
),
],
),
],
),
);
}
/// Tab 4: Official Ministry Textbooks
Widget _buildTextbooksTab(BuildContext context) {
final textbooks = widget.subject.textbooks.isNotEmpty