import 'attempt.dart'; import 'question.dart'; /// Represents an incomplete attempt that can be resumed later. class IncompleteAttempt { /// The attempt ID. final String attemptId; /// List of question IDs in the attempt (in order). final List questionIds; /// Timestamp when the attempt was started. final int startTime; /// Current question index (0-based). final int currentQuestionIndex; /// Map of questionId -> answer (int for single, List for multiple). final Map answers; /// Map of questionId -> manual override (needs practice). final Map manualOverrides; /// Timestamp when the attempt was paused. final int pausedAt; /// Remaining time in seconds (if time limit was set). final int? remainingSeconds; const IncompleteAttempt({ required this.attemptId, required this.questionIds, required this.startTime, required this.currentQuestionIndex, required this.answers, required this.manualOverrides, required this.pausedAt, this.remainingSeconds, }); /// Creates an Attempt from this incomplete attempt using questions from the deck. Attempt toAttempt(List deckQuestions) { final questionMap = {for (var q in deckQuestions) q.id: q}; final questions = questionIds .map((id) => questionMap[id]) .whereType() .toList(); return Attempt( id: attemptId, questions: questions, startTime: startTime, ); } /// Creates an incomplete attempt from JSON. factory IncompleteAttempt.fromJson(Map json) { return IncompleteAttempt( attemptId: json['attemptId'] as String, questionIds: List.from(json['questionIds'] as List), startTime: json['startTime'] as int, currentQuestionIndex: json['currentQuestionIndex'] as int, answers: Map.from(json['answers'] as Map), manualOverrides: Map.from( (json['manualOverrides'] as Map?)?.map((k, v) => MapEntry(k.toString(), v as bool)) ?? {}, ), pausedAt: json['pausedAt'] as int, remainingSeconds: json['remainingSeconds'] as int?, ); } /// Converts to JSON for storage. Map toJson() { return { 'attemptId': attemptId, 'questionIds': questionIds, 'startTime': startTime, 'currentQuestionIndex': currentQuestionIndex, 'answers': answers, 'manualOverrides': manualOverrides, 'pausedAt': pausedAt, if (remainingSeconds != null) 'remainingSeconds': remainingSeconds, }; } /// Creates a copy with updated fields. IncompleteAttempt copyWith({ String? attemptId, List? questionIds, int? startTime, int? currentQuestionIndex, Map? answers, Map? manualOverrides, int? pausedAt, int? remainingSeconds, }) { return IncompleteAttempt( attemptId: attemptId ?? this.attemptId, questionIds: questionIds ?? this.questionIds, startTime: startTime ?? this.startTime, currentQuestionIndex: currentQuestionIndex ?? this.currentQuestionIndex, answers: answers ?? this.answers, manualOverrides: manualOverrides ?? this.manualOverrides, pausedAt: pausedAt ?? this.pausedAt, remainingSeconds: remainingSeconds ?? this.remainingSeconds, ); } @override bool operator ==(Object other) => identical(this, other) || other is IncompleteAttempt && runtimeType == other.runtimeType && attemptId == other.attemptId && questionIds.toString() == other.questionIds.toString() && startTime == other.startTime && currentQuestionIndex == other.currentQuestionIndex && answers.toString() == other.answers.toString() && manualOverrides.toString() == other.manualOverrides.toString() && pausedAt == other.pausedAt && remainingSeconds == other.remainingSeconds; @override int get hashCode => attemptId.hashCode ^ questionIds.hashCode ^ startTime.hashCode ^ currentQuestionIndex.hashCode ^ answers.hashCode ^ manualOverrides.hashCode ^ pausedAt.hashCode ^ (remainingSeconds?.hashCode ?? 0); }