/// An ordered sequence where the chain, not the step, is the unit of /// completion. Breaking mid-way logs partial. class RoutineChain { String? id; String title; List steps; /// Index of the step reached when the chain last broke; -1 when clean. int lastBreakIndex; RoutineChain({ this.id, this.title = "", List? steps, this.lastBreakIndex = -1, }) : steps = steps ?? []; factory RoutineChain.fromJson(Map json) { return RoutineChain( id: json['id'], title: json['title'] ?? "", steps: json['steps'] == null ? [] : (json['steps'] as List) .map((item) => RoutineStep.fromJson(item)) .toList(), lastBreakIndex: json['lastBreakIndex'] ?? -1, ); } Map toJson() { final Map data = {}; data['id'] = id; data['title'] = title; data['steps'] = steps.map((item) => item.toJson()).toList(); data['lastBreakIndex'] = lastBreakIndex; return data; } int get completedSteps { return steps.where((step) => step.completed).length; } /// 0..1 — a partially run chain is recorded as partial, not as done. double get partialCompletion { if (steps.isEmpty) { return 0; } return completedSteps / steps.length; } bool get complete { return steps.isNotEmpty && completedSteps == steps.length; } } class RoutineStep { String? id; String title; int order; int timerSeconds; bool completed; RoutineStep({ this.id, this.title = "", this.order = 0, this.timerSeconds = 0, this.completed = false, }); factory RoutineStep.fromJson(Map json) { return RoutineStep( id: json['id'], title: json['title'] ?? "", order: json['order'] ?? 0, timerSeconds: json['timerSeconds'] ?? 0, completed: json['completed'] ?? false, ); } Map toJson() { final Map data = {}; data['id'] = id; data['title'] = title; data['order'] = order; data['timerSeconds'] = timerSeconds; data['completed'] = completed; return data; } }