BLoC/Cubit state management, ADHD-friendly theme (calming teal, no red), GetIt DI, GoRouter navigation. Screens: task dashboard, focus mode, task create/detail, streaks, time perception, settings, onboarding, auth. Custom widgets: TaskCard, RewardPopup, StreakRing, GentleNudgeCard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
230 lines
8.6 KiB
Dart
230 lines
8.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../../../core/theme/app_colors.dart';
|
|
import '../../../../core/widgets/time_visualizer.dart';
|
|
|
|
/// Time perception tools dashboard.
|
|
///
|
|
/// - Accuracy trend chart (fl_chart) showing estimate vs actual over time.
|
|
/// - "You tend to underestimate by X%" insight.
|
|
/// - Recent time entries list.
|
|
/// - Tips for improving time awareness.
|
|
class TimeDashboardScreen extends StatelessWidget {
|
|
const TimeDashboardScreen({super.key});
|
|
|
|
// ── Dummy data for the chart ─────────────────────────────────────
|
|
static const _estimatedData = [15.0, 20.0, 10.0, 30.0, 25.0, 15.0, 20.0];
|
|
static const _actualData = [22.0, 18.0, 14.0, 40.0, 28.0, 20.0, 19.0];
|
|
static const _labels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
|
|
double get _averageBias {
|
|
double total = 0;
|
|
for (int i = 0; i < _estimatedData.length; i++) {
|
|
total += (_actualData[i] - _estimatedData[i]) / _estimatedData[i];
|
|
}
|
|
return (total / _estimatedData.length) * 100;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final bias = _averageBias;
|
|
final biasText = bias > 0
|
|
? 'You tend to underestimate by ${bias.abs().toStringAsFixed(0)}%'
|
|
: 'You tend to overestimate by ${bias.abs().toStringAsFixed(0)}%';
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back_rounded),
|
|
onPressed: () => context.go('/'),
|
|
),
|
|
title: const Text('Time Perception'),
|
|
),
|
|
body: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
// ── Insight card ─────────────────────────────────────────
|
|
Card(
|
|
color: AppColors.primaryLight.withAlpha(30),
|
|
elevation: 0,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.insights_rounded, size: 32, color: AppColors.primary),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(biasText, style: theme.textTheme.bodyLarge?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// ── Trend chart ──────────────────────────────────────────
|
|
Text('Estimated vs Actual (this week)', style: theme.textTheme.titleMedium),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
height: 220,
|
|
child: LineChart(
|
|
LineChartData(
|
|
gridData: const FlGridData(show: false),
|
|
titlesData: FlTitlesData(
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 32,
|
|
getTitlesWidget: (value, meta) => Text(
|
|
'${value.toInt()}m',
|
|
style: theme.textTheme.bodySmall,
|
|
),
|
|
),
|
|
),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
getTitlesWidget: (value, meta) {
|
|
final idx = value.toInt();
|
|
if (idx < 0 || idx >= _labels.length) return const SizedBox.shrink();
|
|
return Text(_labels[idx], style: theme.textTheme.bodySmall);
|
|
},
|
|
),
|
|
),
|
|
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
|
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
|
),
|
|
borderData: FlBorderData(show: false),
|
|
lineBarsData: [
|
|
// Estimated
|
|
LineChartBarData(
|
|
spots: List.generate(
|
|
_estimatedData.length,
|
|
(i) => FlSpot(i.toDouble(), _estimatedData[i]),
|
|
),
|
|
isCurved: true,
|
|
color: AppColors.primary,
|
|
barWidth: 3,
|
|
dotData: const FlDotData(show: true),
|
|
belowBarData: BarAreaData(
|
|
show: true,
|
|
color: AppColors.primary.withAlpha(20),
|
|
),
|
|
),
|
|
// Actual
|
|
LineChartBarData(
|
|
spots: List.generate(
|
|
_actualData.length,
|
|
(i) => FlSpot(i.toDouble(), _actualData[i]),
|
|
),
|
|
isCurved: true,
|
|
color: AppColors.secondary,
|
|
barWidth: 3,
|
|
dotData: const FlDotData(show: true),
|
|
dashArray: [6, 4],
|
|
),
|
|
],
|
|
minY: 0,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
// ── Legend ────────────────────────────────────────────────
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
_LegendDot(color: AppColors.primary, label: 'Estimated'),
|
|
const SizedBox(width: 20),
|
|
_LegendDot(color: AppColors.secondary, label: 'Actual'),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// ── Recent entries ───────────────────────────────────────
|
|
Text('Recent', style: theme.textTheme.titleMedium),
|
|
const SizedBox(height: 8),
|
|
...List.generate(
|
|
_estimatedData.length,
|
|
(i) => Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: TimeVisualizer(
|
|
estimatedMinutes: _estimatedData[i].toInt(),
|
|
actualMinutes: _actualData[i].toInt(),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// ── Tips ─────────────────────────────────────────────────
|
|
Text('Tips for improving time awareness', style: theme.textTheme.titleMedium),
|
|
const SizedBox(height: 8),
|
|
const _TipCard(
|
|
icon: Icons.timer_outlined,
|
|
text: 'Before starting, say your estimate out loud — it makes you more committed.',
|
|
),
|
|
const _TipCard(
|
|
icon: Icons.visibility_rounded,
|
|
text: 'Use a visible clock or timer while working to build a sense of passing time.',
|
|
),
|
|
const _TipCard(
|
|
icon: Icons.edit_note_rounded,
|
|
text: 'After each task, note how long it really took. Patterns will emerge!',
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _LegendDot extends StatelessWidget {
|
|
final Color color;
|
|
final String label;
|
|
const _LegendDot({required this.color, required this.label});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
|
const SizedBox(width: 6),
|
|
Text(label, style: Theme.of(context).textTheme.bodySmall),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TipCard extends StatelessWidget {
|
|
final IconData icon;
|
|
final String text;
|
|
const _TipCard({required this.icon, required this.text});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Card(
|
|
elevation: 0,
|
|
color: AppColors.surfaceVariantLight,
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(14),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(icon, size: 22, color: AppColors.primary),
|
|
const SizedBox(width: 10),
|
|
Expanded(child: Text(text, style: theme.textTheme.bodyMedium)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|