Files
focusflow/lib/features/time_perception/presentation/screens/time_dashboard_screen.dart
Oracle Public Cloud User 50931d839d Initial scaffold: FocusFlow ADHD Task Manager Flutter app
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>
2026-03-04 15:53:58 +00:00

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)),
],
),
),
);
}
}