Files
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

422 lines
14 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../core/widgets/app_scaffold.dart';
import '../../../core/widgets/loading_indicator.dart';
import '../../../core/widgets/empty_state.dart';
import '../application/insights_controller.dart';
class InsightsScreen extends ConsumerStatefulWidget {
const InsightsScreen({super.key});
@override
ConsumerState<InsightsScreen> createState() => _InsightsScreenState();
}
class _InsightsScreenState extends ConsumerState<InsightsScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(insightsControllerProvider.notifier).refresh();
});
}
@override
Widget build(BuildContext context) {
final state = ref.watch(insightsControllerProvider);
return AppScaffold(
title: 'Insights',
body: state.isLoading
? const LoadingIndicator()
: state.error != null
? _buildError(state.error!)
: _buildContent(state),
);
}
Widget _buildError(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Error loading insights',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(error),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
ref.read(insightsControllerProvider.notifier).refresh();
},
child: const Text('Retry'),
),
],
),
);
}
Widget _buildContent(InsightsState state) {
if (state.totalGoals == 0) {
return const EmptyState(
icon: Icons.insights_outlined,
title: 'No data yet',
subtitle: 'Start creating goals to see your insights',
);
}
return RefreshIndicator(
onRefresh: () async {
await ref.read(insightsControllerProvider.notifier).refresh();
},
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildOverviewCards(state),
const SizedBox(height: 24),
_buildProgressChart(state),
const SizedBox(height: 24),
_buildGoalCompletionTrends(state),
const SizedBox(height: 24),
_buildStreakCard(state),
],
),
),
);
}
Widget _buildOverviewCards(InsightsState state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Overview',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatCard(
'Total Goals',
state.totalGoals.toString(),
Icons.flag_outlined,
Theme.of(context).colorScheme.primary,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'Completed',
state.completedGoals.toString(),
Icons.check_circle_outline,
Colors.green,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildStatCard(
'Active',
state.activeGoals.toString(),
Icons.pending_outlined,
Colors.orange,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'Progress',
'${state.overallProgress.toStringAsFixed(0)}%',
Icons.trending_up,
Colors.blue,
),
),
],
),
],
);
}
Widget _buildStatCard(String title, String value, IconData icon, Color color) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 8),
Text(
value,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}
Widget _buildProgressChart(InsightsState state) {
final trends = ref.read(insightsControllerProvider.notifier).getProgressVsTimeData();
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Progress vs Time',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: trends.isEmpty
? const Center(child: Text('No countdown data available'))
: LineChart(
LineChartData(
gridData: const FlGridData(show: true),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: (value, meta) {
if (value % 20 == 0) {
return Text(
'${value.toInt()}%',
style: const TextStyle(fontSize: 10),
);
}
return const Text('');
},
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (value, meta) {
if (value.toInt() % 2 == 0) {
return Text(
'Day ${value.toInt()}',
style: const TextStyle(fontSize: 10),
);
}
return const Text('');
},
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(show: true),
lineBarsData: [
LineChartBarData(
spots: trends
.map((t) => FlSpot(t['day'].toDouble(), t['expected'].toDouble()))
.toList(),
isCurved: true,
color: Colors.grey[400],
barWidth: 2,
dotData: const FlDotData(show: false),
),
LineChartBarData(
spots: trends
.map((t) => FlSpot(t['day'].toDouble(), t['actual'].toDouble()))
.toList(),
isCurved: true,
color: Theme.of(context).colorScheme.primary,
barWidth: 3,
dotData: const FlDotData(show: true),
),
],
),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildLegendItem('Expected', Colors.grey[400]!),
const SizedBox(width: 16),
_buildLegendItem('Actual', Theme.of(context).colorScheme.primary),
],
),
],
),
),
);
}
Widget _buildLegendItem(String label, Color color) {
return Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
Text(
label,
style: Theme.of(context).textTheme.bodySmall,
),
],
);
}
Widget _buildGoalCompletionTrends(InsightsState state) {
final trends = ref.read(insightsControllerProvider.notifier).getGoalCompletionTrends();
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Weekly Completion Trends',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: trends.isEmpty
? const Center(child: Text('No data available'))
: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: trends.map((t) => t['completed'] as int).reduce((a, b) => a > b ? a : b).toDouble() + 1,
barGroups: trends.asMap().entries.map((entry) {
return BarChartGroupData(
x: entry.key,
barRods: [
BarChartRodData(
toY: entry.value['completed'].toDouble(),
color: Theme.of(context).colorScheme.primary,
width: 20,
borderRadius: BorderRadius.circular(4),
),
],
);
}).toList(),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: (value, meta) {
if (value == value.toInt()) {
return Text(
value.toInt().toString(),
style: const TextStyle(fontSize: 10),
);
}
return const Text('');
},
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (value, meta) {
if (value >= 0 && value < trends.length) {
return Text(
trends[value.toInt()]['week'] as String,
style: const TextStyle(fontSize: 10),
);
}
return const Text('');
},
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(show: true),
),
),
),
],
),
),
);
}
Widget _buildStreakCard(InsightsState state) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
const Icon(
Icons.local_fire_department,
size: 48,
color: Colors.orange,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Current Streak',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(
'${state.currentStreak} days',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.orange,
),
),
const SizedBox(height: 8),
Text(
'Longest: ${state.longestStreak} days',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
),
);
}
}