/// ============================================================================== /// SAQEL ENTERPRISE (EDTECH 2.0) - ADAPTIVE EXAM STATE MANAGEMENT (CUBIT) /// ============================================================================== /// /// ملف: exam_cubit.dart /// الهدف المعماري: /// إدارة حالة دورة حياة الامتحانات التكيفية والتشخيص الذكي في تطبيق الطالب: /// 1. جلب الامتحانات مباشرة من واجهات السيرفر (Pure API-Driven) دون أي بيانات أو أسئلة محلية ثابتة. /// 2. إدارة المؤقت التنازلي الذكي ومزامنة الوقت المتبقي. /// 3. إدارة التنقل بين الأسئلة وحفظ اختيارات الطالب وتحديد البدائل المحددة. /// 4. إرسال الإجابات إلى السيرفر لتصحيحها واستلام تقرير التشخيص السقراطي ومؤشر الجاهزية للتوجيهي. library; import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../data/models/exam_model.dart'; import '../../data/repositories/app_repositories.dart'; /// الحالات العامة للامتحان abstract class ExamState {} /// الحالة المبدئية قبل بدء التحميل class ExamInitial extends ExamState {} /// حالة انتظار جلب الأسئلة من السيرفر class ExamLoading extends ExamState { final String message; ExamLoading({this.message = 'جاري تحضير الامتحان والأسئلة التكيفية من السيرفر...'}); } class ExamLoaded extends ExamState { final ExamModel exam; final int currentQuestionIndex; final Map selectedAnswers; // questionId -> optionId final int remainingSeconds; final bool isSubmitting; ExamLoaded({ required this.exam, this.currentQuestionIndex = 0, this.selectedAnswers = const {}, required this.remainingSeconds, this.isSubmitting = false, }); QuestionModel? get currentQuestion { if (exam.questions.isEmpty || currentQuestionIndex >= exam.questions.length) return null; return exam.questions[currentQuestionIndex]; } bool get isLastQuestion => currentQuestionIndex == exam.questions.length - 1; int get answeredCount => selectedAnswers.length; ExamLoaded copyWith({ ExamModel? exam, int? currentQuestionIndex, Map? selectedAnswers, int? remainingSeconds, bool? isSubmitting, }) { return ExamLoaded( exam: exam ?? this.exam, currentQuestionIndex: currentQuestionIndex ?? this.currentQuestionIndex, selectedAnswers: selectedAnswers ?? this.selectedAnswers, remainingSeconds: remainingSeconds ?? this.remainingSeconds, isSubmitting: isSubmitting ?? this.isSubmitting, ); } } class ExamCompleted extends ExamState { final ExamModel exam; final ExamSubmissionResultModel result; final int timeSpentSeconds; ExamCompleted({ required this.exam, required this.result, required this.timeSpentSeconds, }); } class ExamError extends ExamState { final String message; ExamError(this.message); } class ExamCubit extends Cubit { final ExamRepository _repository; Timer? _countdownTimer; int _initialSeconds = 0; ExamCubit({ExamRepository? repository}) : _repository = repository ?? ExamRepository(), super(ExamInitial()); @override Future close() { _countdownTimer?.cancel(); return super.close(); } Future loadExam({int examId = 1, ExamModel? initialExam}) async { emit(ExamLoading()); try { ExamModel loadedExam; if (initialExam != null && initialExam.questions.isNotEmpty) { loadedExam = initialExam; } else { loadedExam = await _repository.getExamDetails(examId); } if (loadedExam.questions.isEmpty) { emit(ExamError('لم يتم العثور على أسئلة لهذا الامتحان في السيرفر. يرجى توليد بنك الأسئلة من لوحة المناهج.')); return; } _initialSeconds = loadedExam.durationMinutes * 60; emit(ExamLoaded( exam: loadedExam, remainingSeconds: _initialSeconds, )); _startTimer(); } catch (e) { emit(ExamError('تعذر جلب الامتحان من السيرفر: ${e.toString()}')); } } void _startTimer() { _countdownTimer?.cancel(); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { final s = state; if (s is ExamLoaded) { if (s.remainingSeconds <= 1) { timer.cancel(); submitCurrentExam(); } else { emit(s.copyWith(remainingSeconds: s.remainingSeconds - 1)); } } else { timer.cancel(); } }); } void selectOption(int questionId, int optionId) { final s = state; if (s is ExamLoaded) { final updatedAnswers = Map.from(s.selectedAnswers); updatedAnswers[questionId] = optionId; emit(s.copyWith(selectedAnswers: updatedAnswers)); } } void goToQuestion(int index) { final s = state; if (s is ExamLoaded) { if (index >= 0 && index < s.exam.questions.length) { emit(s.copyWith(currentQuestionIndex: index)); } } } void nextQuestion() { final s = state; if (s is ExamLoaded && !s.isLastQuestion) { emit(s.copyWith(currentQuestionIndex: s.currentQuestionIndex + 1)); } } void previousQuestion() { final s = state; if (s is ExamLoaded && s.currentQuestionIndex > 0) { emit(s.copyWith(currentQuestionIndex: s.currentQuestionIndex - 1)); } } Future submitCurrentExam() async { final s = state; if (s is! ExamLoaded) return; _countdownTimer?.cancel(); emit(s.copyWith(isSubmitting: true)); final timeSpent = _initialSeconds - s.remainingSeconds; final formattedAnswers = s.selectedAnswers.entries.map((e) { return { 'question_id': e.key, 'selected_option_id': e.value, }; }).toList(); try { final result = await _repository.submitExam( s.exam.id, answers: formattedAnswers, timeSpentSeconds: timeSpent, ); emit(ExamCompleted( exam: s.exam, result: result, timeSpentSeconds: timeSpent, )); } catch (e) { emit(ExamError('فشل تقديم الامتحان: ${e.toString()}')); } } }