Files
1356/lifetimer/lib/features/analytics/application/insights_controller.dart
T
Tomas Dvorak 37ffb93923 feat: Complete Phase 1 - Full Flutter app implementation with comprehensive features
Version: 1.1.0

Major changes:
- Implemented complete Flutter app structure with all core features
- Added comprehensive UI screens for auth, countdown, goals, profile, settings, and social features
- Integrated Supabase backend with authentication and data repositories
- Added offline support with Hive caching and local storage
- Implemented comprehensive routing with go_router
- Added location services with Google Maps integration
- Implemented notifications and home widget support
- Added voice recording capabilities and AI chat features
- Created comprehensive test suite and documentation
- Added Android and iOS platform configurations
- Implemented achievements system and social features
- Added calendar integration and bucket list functionality

This represents a complete Phase 1 milestone with 3,775 additions across 31 files.
2026-01-04 14:33:54 +01:00

280 lines
8.2 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../data/models/goal_model.dart';
import '../../../data/repositories/goals_repository.dart';
import '../../../data/repositories/countdown_repository.dart';
import '../../../bootstrap/supabase_client.dart';
import '../../auth/application/auth_controller.dart';
class InsightsState {
final bool isLoading;
final String? error;
final List<Goal> goals;
final int totalGoals;
final int completedGoals;
final int activeGoals;
final double overallProgress;
final int currentStreak;
final int longestStreak;
final DateTime? countdownStartDate;
final DateTime? countdownEndDate;
final int daysRemaining;
final double timeElapsedPercentage;
const InsightsState({
this.isLoading = false,
this.error,
this.goals = const [],
this.totalGoals = 0,
this.completedGoals = 0,
this.activeGoals = 0,
this.overallProgress = 0.0,
this.currentStreak = 0,
this.longestStreak = 0,
this.countdownStartDate,
this.countdownEndDate,
this.daysRemaining = 0,
this.timeElapsedPercentage = 0.0,
});
InsightsState copyWith({
bool? isLoading,
String? error,
List<Goal>? goals,
int? totalGoals,
int? completedGoals,
int? activeGoals,
double? overallProgress,
int? currentStreak,
int? longestStreak,
DateTime? countdownStartDate,
DateTime? countdownEndDate,
int? daysRemaining,
double? timeElapsedPercentage,
}) {
return InsightsState(
isLoading: isLoading ?? this.isLoading,
error: error,
goals: goals ?? this.goals,
totalGoals: totalGoals ?? this.totalGoals,
completedGoals: completedGoals ?? this.completedGoals,
activeGoals: activeGoals ?? this.activeGoals,
overallProgress: overallProgress ?? this.overallProgress,
currentStreak: currentStreak ?? this.currentStreak,
longestStreak: longestStreak ?? this.longestStreak,
countdownStartDate: countdownStartDate ?? this.countdownStartDate,
countdownEndDate: countdownEndDate ?? this.countdownEndDate,
daysRemaining: daysRemaining ?? this.daysRemaining,
timeElapsedPercentage: timeElapsedPercentage ?? this.timeElapsedPercentage,
);
}
}
class InsightsController extends StateNotifier<InsightsState> {
final GoalsRepository _goalsRepository;
final CountdownRepository _countdownRepository;
final AuthController _authController;
InsightsController(
this._goalsRepository,
this._countdownRepository,
this._authController,
) : super(const InsightsState()) {
_loadInsights();
}
Future<void> _loadInsights() async {
final userId = _authController.currentUserId;
if (userId == null) return;
state = state.copyWith(isLoading: true);
try {
final goals = await _goalsRepository.getGoals(userId);
final countdown = await _countdownRepository.getCountdownInfo(userId);
final totalGoals = goals.length;
final completedGoals = goals.where((g) => g.completed).length;
final activeGoals = totalGoals - completedGoals;
final overallProgress = totalGoals > 0
? (completedGoals / totalGoals) * 100
: 0.0;
final currentStreak = _calculateCurrentStreak(goals);
final longestStreak = _calculateLongestStreak(goals);
final daysRemaining = countdown.daysRemaining ?? 0;
final totalDays = countdown.countdownEndDate != null && countdown.countdownStartDate != null
? countdown.countdownEndDate!.difference(countdown.countdownStartDate!).inDays
: 0;
final elapsedDays = countdown.countdownStartDate != null
? DateTime.now().difference(countdown.countdownStartDate!).inDays.clamp(0, totalDays)
: 0;
final timeElapsedPercentage = totalDays > 0
? (elapsedDays / totalDays) * 100
: 0.0;
state = state.copyWith(
isLoading: false,
goals: goals,
totalGoals: totalGoals,
completedGoals: completedGoals,
activeGoals: activeGoals,
overallProgress: overallProgress,
currentStreak: currentStreak,
longestStreak: longestStreak,
countdownStartDate: countdown.countdownStartDate,
countdownEndDate: countdown.countdownEndDate,
daysRemaining: daysRemaining,
timeElapsedPercentage: timeElapsedPercentage,
);
} catch (e) {
state = state.copyWith(
isLoading: false,
error: e.toString(),
);
}
}
int _calculateCurrentStreak(List<Goal> goals) {
if (goals.isEmpty) return 0;
final now = DateTime.now();
int streak = 0;
DateTime currentDate = now;
for (int i = 0; i < 365; i++) {
final hasActivityOnDay = goals.any((goal) {
final updatedDate = goal.updatedAt;
return updatedDate.year == currentDate.year &&
updatedDate.month == currentDate.month &&
updatedDate.day == currentDate.day;
});
if (hasActivityOnDay) {
streak++;
currentDate = currentDate.subtract(const Duration(days: 1));
} else {
break;
}
}
return streak;
}
int _calculateLongestStreak(List<Goal> goals) {
if (goals.isEmpty) return 0;
final allDates = goals
.map((g) => g.updatedAt)
.whereType<DateTime>()
.toSet()
.toList()
..sort();
if (allDates.isEmpty) return 0;
int longestStreak = 1;
int currentStreak = 1;
for (int i = 1; i < allDates.length; i++) {
final difference = allDates[i].difference(allDates[i - 1]).inDays;
if (difference == 1) {
currentStreak++;
} else if (difference > 1) {
longestStreak = longestStreak > currentStreak ? longestStreak : currentStreak;
currentStreak = 1;
}
}
return longestStreak > currentStreak ? longestStreak : currentStreak;
}
List<Map<String, dynamic>> getGoalCompletionTrends() {
final goals = state.goals;
if (goals.isEmpty) return [];
final now = DateTime.now();
final trends = <Map<String, dynamic>>[];
for (int i = 6; i >= 0; i--) {
final weekStart = now.subtract(Duration(days: i * 7));
final weekEnd = weekStart.add(const Duration(days: 7));
final completedInWeek = goals.where((goal) {
final updated = goal.updatedAt;
return goal.completed &&
updated.isAfter(weekStart) &&
updated.isBefore(weekEnd);
}).length;
trends.add({
'week': 'Week ${7 - i}',
'completed': completedInWeek,
});
}
return trends;
}
List<Map<String, dynamic>> getProgressVsTimeData() {
if (state.countdownStartDate == null || state.countdownEndDate == null) {
return [];
}
final start = state.countdownStartDate!;
final end = state.countdownEndDate!;
final totalDays = end.difference(start).inDays;
final elapsedDays = DateTime.now().difference(start).inDays.clamp(0, totalDays);
final data = <Map<String, dynamic>>[];
const int intervals = 10;
for (int i = 0; i <= intervals; i++) {
final day = (totalDays * i / intervals).round();
final date = start.add(Duration(days: day));
final expectedProgress = (i / intervals) * 100;
final actualProgress = i <= (elapsedDays / totalDays * intervals).round()
? state.overallProgress
: 0.0;
data.add({
'day': day,
'date': date,
'expected': expectedProgress,
'actual': actualProgress,
});
}
return data;
}
void clearError() {
state = state.copyWith(error: null);
}
Future<void> refresh() async {
await _loadInsights();
}
}
final insightsControllerProvider =
StateNotifierProvider<InsightsController, InsightsState>((ref) {
final goalsRepository = ref.watch(goalsRepositoryProvider);
final countdownRepository = ref.watch(countdownRepositoryProvider);
final authController = ref.watch(authControllerProvider.notifier);
return InsightsController(
goalsRepository,
countdownRepository,
authController,
);
});
final goalsRepositoryProvider = Provider<GoalsRepository>((ref) {
return GoalsRepository(supabaseClient);
});
final countdownRepositoryProvider = Provider<CountdownRepository>((ref) {
return CountdownRepository(supabaseClient);
});