176 lines
4.3 KiB
Dart
176 lines
4.3 KiB
Dart
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
import 'package:hive/hive.dart';
|
|
import 'routine.dart';
|
|
|
|
part 'activity.freezed.dart';
|
|
part 'activity.g.dart';
|
|
|
|
@HiveType(typeId: 5)
|
|
enum Mood {
|
|
@HiveField(0)
|
|
terrible,
|
|
@HiveField(1)
|
|
bad,
|
|
@HiveField(2)
|
|
neutral,
|
|
@HiveField(3)
|
|
good,
|
|
@HiveField(4)
|
|
great,
|
|
}
|
|
|
|
@HiveType(typeId: 6)
|
|
@freezed
|
|
class Activity with _$Activity {
|
|
const factory Activity({
|
|
@HiveField(0) required String id,
|
|
@HiveField(1) required String routineId,
|
|
@HiveField(2) required DateTime timestamp,
|
|
@HiveField(3) @Default(true) bool completed,
|
|
@HiveField(4) String? notes,
|
|
@HiveField(5) Mood? mood,
|
|
@HiveField(6) @Default(0) int pointsEarned,
|
|
@HiveField(7) DateTime? scheduledTime,
|
|
}) = _Activity;
|
|
|
|
factory Activity.fromJson(Map<String, dynamic> json) =
|
|
_$$ActivityImplFromJson;
|
|
}
|
|
|
|
extension MoodExtension on Mood {
|
|
String get displayName {
|
|
switch (this) {
|
|
case Mood.terrible:
|
|
return 'Terrible';
|
|
case Mood.bad:
|
|
return 'Bad';
|
|
case Mood.neutral:
|
|
return 'Okay';
|
|
case Mood.good:
|
|
return 'Good';
|
|
case Mood.great:
|
|
return 'Great!';
|
|
}
|
|
}
|
|
|
|
String get emoji {
|
|
switch (this) {
|
|
case Mood.terrible:
|
|
return '😫';
|
|
case Mood.bad:
|
|
return '😕';
|
|
case Mood.neutral:
|
|
return '😐';
|
|
case Mood.good:
|
|
return '🙂';
|
|
case Mood.great:
|
|
return '😄';
|
|
}
|
|
}
|
|
}
|
|
|
|
@HiveType(typeId: 7)
|
|
@freezed
|
|
class ActivityStats with _$ActivityStats {
|
|
const factory ActivityStats({
|
|
@HiveField(0) required int totalCompleted,
|
|
@HiveField(1) required int totalSkipped,
|
|
@HiveField(2) required double completionRate,
|
|
@HiveField(3) required int currentStreak,
|
|
@HiveField(4) required int longestStreak,
|
|
@HiveField(5) required int totalPoints,
|
|
@HiveField(6) @Default({}) Map<String, int> categoryCompletion,
|
|
}) = _ActivityStats;
|
|
|
|
factory ActivityStats.fromJson(Map<String, dynamic> json) =
|
|
_$$ActivityStatsImplFromJson;
|
|
}
|
|
|
|
// Helper class for daily progress
|
|
class DailyProgress {
|
|
final DateTime date;
|
|
final int totalRoutines;
|
|
final int completedRoutines;
|
|
final int totalPoints;
|
|
final bool allCompleted;
|
|
|
|
DailyProgress({
|
|
required this.date,
|
|
required this.totalRoutines,
|
|
required this.completedRoutines,
|
|
required this.totalPoints,
|
|
}) : allCompleted = totalRoutines > 0 && totalRoutines == completedRoutines;
|
|
|
|
double get completionPercentage =
|
|
totalRoutines > 0 ? (completedRoutines / totalRoutines) * 100 : 0;
|
|
}
|
|
|
|
// Helper for streak calculation
|
|
class StreakCalculator {
|
|
static int calculateCurrentStreak(List<Activity> activities) {
|
|
if (activities.isEmpty) return 0;
|
|
|
|
// Sort by date descending
|
|
final sorted = activities.toList()
|
|
..sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
|
|
|
int streak = 0;
|
|
DateTime checkDate = DateTime.now();
|
|
|
|
for (final activity in sorted) {
|
|
final activityDate = DateTime(
|
|
activity.timestamp.year,
|
|
activity.timestamp.month,
|
|
activity.timestamp.day,
|
|
);
|
|
|
|
final checkDateOnly = DateTime(
|
|
checkDate.year,
|
|
checkDate.month,
|
|
checkDate.day,
|
|
);
|
|
|
|
if (activityDate.isAtSameMomentAs(checkDateOnly) && activity.completed) {
|
|
streak++;
|
|
checkDate = checkDate.subtract(const Duration(days: 1));
|
|
} else if (activityDate.isBefore(checkDateOnly)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return streak;
|
|
}
|
|
|
|
static int calculateLongestStreak(List<Activity> activities) {
|
|
if (activities.isEmpty) return 0;
|
|
|
|
// Group by day
|
|
final dailyCompletion = <DateTime, bool>{};
|
|
for (final activity in activities) {
|
|
final date = DateTime(
|
|
activity.timestamp.year,
|
|
activity.timestamp.month,
|
|
activity.timestamp.day,
|
|
);
|
|
dailyCompletion[date] = (dailyCompletion[date] ?? true) && activity.completed;
|
|
}
|
|
|
|
// Sort dates
|
|
final dates = dailyCompletion.keys.toList()..sort();
|
|
|
|
int longestStreak = 0;
|
|
int currentStreak = 0;
|
|
|
|
for (int i = 0; i < dates.length; i++) {
|
|
if (dailyCompletion[dates[i]] == true) {
|
|
currentStreak++;
|
|
longestStreak = longestStreak > currentStreak ? longestStreak : currentStreak;
|
|
} else {
|
|
currentStreak = 0;
|
|
}
|
|
}
|
|
|
|
return longestStreak;
|
|
}
|
|
}
|