487 lines
16 KiB
Dart
487 lines
16 KiB
Dart
// ignore_for_file: cascade_invocations
|
|
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:habitrack_app/infrastructure/widget_wall/items_controller.dart';
|
|
import 'package:habitrack_app/infrastructure/widget_wall/items_state.dart';
|
|
import 'package:habitrack_app/main.dart';
|
|
import 'package:habitrack_app/sembast/timer.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
class DataPoint {
|
|
DataPoint({required this.date, required this.progress});
|
|
DateTime date;
|
|
double progress;
|
|
}
|
|
|
|
class TimerGraphWidget extends ConsumerStatefulWidget {
|
|
TimerGraphWidget({super.key});
|
|
|
|
@override
|
|
ConsumerState<ConsumerStatefulWidget> createState() =>
|
|
_TimerGraphWidgetState();
|
|
|
|
final entries = <DataPoint>[
|
|
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
|
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
|
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
|
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
|
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
|
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
|
];
|
|
}
|
|
|
|
class _TimerGraphWidgetState extends ConsumerState<TimerGraphWidget> {
|
|
final gradientColors = [
|
|
Colors.cyan,
|
|
Colors.blueAccent,
|
|
];
|
|
final gradient2Colors = [
|
|
Colors.amber,
|
|
Colors.amberAccent,
|
|
];
|
|
String _formatTime() {
|
|
final minutesTotal =
|
|
(_selectedValue == 'weekly') ? _timePlanned : _todayGoal;
|
|
final hours = (minutesTotal / 60).floor();
|
|
final minutes = (minutesTotal - (hours * 60)).toInt();
|
|
return '$hours hours : $minutes minutes';
|
|
}
|
|
|
|
String _formatCurrent() {
|
|
final secondsTotal =
|
|
(_selectedValue == 'weekly') ? _timeCompleted : _todayCurrent;
|
|
final hours = (secondsTotal / 3600).floor();
|
|
final minutes = ((secondsTotal - (hours * 3600)) / 60).floor();
|
|
return '$hours hours : $minutes minutes';
|
|
}
|
|
|
|
void _buildList(List<dynamic> value) {
|
|
final items = value;
|
|
_timeCompleted = 0;
|
|
_timePlanned = 0;
|
|
|
|
setState(() {
|
|
_todayCurrent = 0;
|
|
_todayGoal = 0;
|
|
_weeklyPlannedEntries = [];
|
|
_weeklyWorkedEntries = [];
|
|
for (var i = 0; i <= 6; i++) {
|
|
final itemToInsert = DataPoint(
|
|
date: _latestDate.subtract(Duration(days: 6 - i)),
|
|
progress: 0,
|
|
);
|
|
|
|
_weeklyPlannedEntries.add(itemToInsert);
|
|
}
|
|
|
|
for (var i = 0; i <= 6; i++) {
|
|
final itemToInsert = DataPoint(
|
|
date: _latestDate.subtract(Duration(days: 6 - i)),
|
|
progress: 0,
|
|
);
|
|
_weeklyWorkedEntries.add(itemToInsert);
|
|
}
|
|
});
|
|
for (final item in items) {
|
|
if (_selectedValue == 'weekly' && item is TimerItem) {
|
|
final parsedDate = DateTime.parse(item.createdOn);
|
|
for (var i = 0; i <= 6; i++) {
|
|
if (parsedDate.isBefore(_latestDate.subtract(Duration(days: i))) &&
|
|
parsedDate.isAfter(_latestDate.subtract(Duration(days: i + 1)))) {
|
|
logger.i('LOOPING');
|
|
|
|
_timeCompleted += item.current;
|
|
_timePlanned += item.goal;
|
|
// _maxTasks += 1;
|
|
final hours = (item.goal / 60).ceil().toDouble();
|
|
|
|
if (hours > _maxTime) {
|
|
_maxTime = hours;
|
|
}
|
|
|
|
// Update maxAmount
|
|
setState(() {
|
|
_weeklyPlannedEntries.elementAt(6).progress = item.goal / 60;
|
|
_weeklyWorkedEntries.elementAt(6).progress = item.current / 3600;
|
|
|
|
// _weeklyWorkedEntries.elementAt(6 - i).progress =
|
|
// _tasksCompleted.toDouble();
|
|
});
|
|
}
|
|
}
|
|
} else if (_selectedValue == 'daily' && item is TimerItem) {
|
|
final parsedDate = DateTime.parse(item.createdOn);
|
|
|
|
final oneDayAgo = _latestDate.subtract(const Duration(days: 1));
|
|
|
|
if (parsedDate.isBefore(oneDayAgo)) {
|
|
logger.i('More than a day old');
|
|
} else if (parsedDate.isAfter(oneDayAgo) &&
|
|
parsedDate.isBefore(_latestDate)) {
|
|
logger.i('TOday');
|
|
_todayCurrent += item.current;
|
|
_todayGoal += item.goal;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void _showPreviousWeek() {
|
|
if (_selectedValue == 'weekly') {
|
|
_latestDate = _latestDate.subtract(const Duration(days: 7));
|
|
} else if (_selectedValue == 'daily') {
|
|
logger.i('HMMM');
|
|
_latestDate = _latestDate.subtract(const Duration(days: 1));
|
|
}
|
|
final items = ref.watch(itemsProvider);
|
|
|
|
switch (items) {
|
|
case AsyncError(:final error):
|
|
logger.i('Error: $error');
|
|
case AsyncData(:final value):
|
|
final allItems = value;
|
|
|
|
_buildList(allItems);
|
|
default:
|
|
logger.i('Hmmm, how can we help?');
|
|
} // get current date
|
|
// show past 7 days starting 14 days ago
|
|
}
|
|
|
|
void _showNextWeek() {
|
|
logger.i('CURRENT: ${_todayCurrent / 60}');
|
|
|
|
logger.i('MINUTES TOTAL: $_todayGoal');
|
|
logger.i('PERCENTAGE: ${_todayCurrent / (_todayGoal * 60)}');
|
|
if (_selectedValue == 'weekly') {
|
|
_latestDate = _latestDate.add(const Duration(days: 7));
|
|
} else if (_selectedValue == 'daily') {
|
|
logger.i('HMMM');
|
|
_latestDate = _latestDate.add(const Duration(days: 1));
|
|
}
|
|
if (!_latestDate.isAfter(DateTime.now())) {
|
|
final items = ref.watch(itemsProvider);
|
|
|
|
ref.watch(homeControllerProvider);
|
|
switch (items) {
|
|
case AsyncError(:final error):
|
|
logger.i('Error: $error');
|
|
case AsyncData(:final value):
|
|
final allItems = value;
|
|
|
|
_buildList(allItems);
|
|
default:
|
|
logger.i('Hmmm, how can we help?');
|
|
} // get current date
|
|
}
|
|
|
|
// show past 7 days starting 14 days ago
|
|
}
|
|
|
|
String _getText() {
|
|
final dateFormat = DateFormat('dd. MMM yyyy');
|
|
|
|
if (_selectedValue == 'weekly' &&
|
|
_latestDate.isAfter(DateTime.now().subtract(const Duration(days: 6)))) {
|
|
return ' this past week';
|
|
} else if (_selectedValue == 'weekly' &&
|
|
!_latestDate
|
|
.isAfter(DateTime.now().subtract(const Duration(days: 6)))) {
|
|
return ' '
|
|
'from'
|
|
' ${dateFormat.format(_latestDate.subtract(const Duration(days: 6)))}'
|
|
' to ${dateFormat.format(_latestDate)}';
|
|
}
|
|
final formattedDate = dateFormat.format(_latestDate);
|
|
// return ' on ${_latestDate.toString().substring(6, 10)}';
|
|
return ' on $formattedDate';
|
|
}
|
|
|
|
List<dynamic> thisWeekItems = [];
|
|
List<dynamic> todayItems = [];
|
|
String? _selectedValue = 'weekly';
|
|
|
|
int _timeCompleted = 0;
|
|
int _timePlanned = 0;
|
|
double _maxTime = 0;
|
|
DateTime _latestDate = DateTime.now();
|
|
double _todayCurrent = 0;
|
|
double _todayGoal = 0;
|
|
|
|
List<DataPoint> _weeklyPlannedEntries = <DataPoint>[];
|
|
List<DataPoint> _weeklyWorkedEntries = <DataPoint>[];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final items = ref.watch(itemsProvider);
|
|
|
|
// ignore: unused_element
|
|
double getProgress() {
|
|
logger.i('CURRENT: $_todayCurrent');
|
|
logger.i('GOAL: $_todayGoal');
|
|
if (_todayGoal == 0) {
|
|
return 0;
|
|
}
|
|
return _todayCurrent / _todayGoal;
|
|
}
|
|
|
|
ref.watch(homeControllerProvider);
|
|
switch (items) {
|
|
case AsyncError(:final error):
|
|
logger.i('Error: $error');
|
|
case AsyncData(:final value):
|
|
final allItems = value;
|
|
_buildList(allItems);
|
|
default:
|
|
logger.i('Hmmm, how can we help?');
|
|
}
|
|
|
|
final firstDate = _weeklyPlannedEntries[0].date;
|
|
|
|
final flSpots = <FlSpot>[];
|
|
for (final dataPoint in _weeklyPlannedEntries) {
|
|
final xValue = _daysBetween(_weeklyPlannedEntries[0].date, dataPoint.date)
|
|
.toDouble();
|
|
final lcbd = FlSpot(
|
|
xValue,
|
|
dataPoint.progress,
|
|
);
|
|
|
|
flSpots.add(lcbd);
|
|
}
|
|
final weeklyWorkedSpots = <FlSpot>[];
|
|
for (final dataPoint in _weeklyWorkedEntries) {
|
|
final xValue =
|
|
_daysBetween(_weeklyWorkedEntries[0].date, dataPoint.date).toDouble();
|
|
final lcbd = FlSpot(xValue, dataPoint.progress);
|
|
|
|
weeklyWorkedSpots.add(lcbd);
|
|
}
|
|
|
|
final df = DateFormat('dd. MMMM');
|
|
|
|
return Column(
|
|
children: [
|
|
Container(
|
|
height: 75,
|
|
color: Theme.of(context).colorScheme.primaryContainer,
|
|
margin: const EdgeInsets.only(bottom: 10),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
ElevatedButton(
|
|
onPressed: _showPreviousWeek,
|
|
style: ButtonStyle(
|
|
shape: WidgetStateProperty.all(
|
|
RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
),
|
|
),
|
|
child: const Text('Previous'),
|
|
),
|
|
DropdownButton<String>(
|
|
value: _selectedValue,
|
|
borderRadius: BorderRadius.circular(10),
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_latestDate = DateTime.now();
|
|
_selectedValue = value;
|
|
});
|
|
},
|
|
items: const [
|
|
DropdownMenuItem(
|
|
value: 'daily',
|
|
child: Text('Daily'),
|
|
),
|
|
DropdownMenuItem(
|
|
value: 'weekly',
|
|
child: Text('Weekly'),
|
|
),
|
|
],
|
|
),
|
|
ElevatedButton(
|
|
onPressed: _showNextWeek,
|
|
style: ButtonStyle(
|
|
shape: WidgetStateProperty.all(
|
|
RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
),
|
|
),
|
|
child: const Text('Next'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (_selectedValue == 'weekly') ...[
|
|
Container(
|
|
padding:
|
|
const EdgeInsets.only(bottom: 15, top: 15, right: 15, left: 5),
|
|
child: SizedBox(
|
|
width: MediaQuery.of(context).size.width * 0.90,
|
|
height: MediaQuery.of(context).size.height * 0.5,
|
|
child: LineChart(
|
|
LineChartData(
|
|
minY: 0,
|
|
maxY: _maxTime,
|
|
lineBarsData: [
|
|
LineChartBarData(
|
|
isCurved: true,
|
|
preventCurveOverShooting: true,
|
|
spots: flSpots,
|
|
belowBarData: BarAreaData(
|
|
show: true,
|
|
gradient: LinearGradient(
|
|
colors: gradientColors
|
|
.map((color) => color.withOpacity(0.3))
|
|
.toList(),
|
|
),
|
|
),
|
|
),
|
|
LineChartBarData(
|
|
color: Colors.amber,
|
|
isCurved: true,
|
|
isStrokeCapRound: true,
|
|
preventCurveOverShooting: true,
|
|
spots: weeklyWorkedSpots,
|
|
belowBarData: BarAreaData(
|
|
show: true,
|
|
gradient: LinearGradient(
|
|
colors: gradient2Colors
|
|
.map((color) => color.withOpacity(0.3))
|
|
.toList(),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
titlesData: FlTitlesData(
|
|
rightTitles: const AxisTitles(
|
|
sideTitles: SideTitles(
|
|
reservedSize: 30,
|
|
interval: 20,
|
|
),
|
|
),
|
|
topTitles: const AxisTitles(
|
|
sideTitles: SideTitles(
|
|
interval: 20,
|
|
),
|
|
),
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 50,
|
|
getTitlesWidget: (value, meta) => Text(
|
|
'$value h',
|
|
style: const TextStyle(
|
|
fontSize: 10,
|
|
fontStyle: FontStyle.italic,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
interval: 0.5,
|
|
),
|
|
),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
reservedSize: 30,
|
|
getTitlesWidget: (value, meta) => Text(
|
|
style: const TextStyle(
|
|
fontSize: 10,
|
|
fontStyle: FontStyle.italic,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
df.format(
|
|
DateTime(
|
|
firstDate.year,
|
|
firstDate.month,
|
|
firstDate.day,
|
|
).add(Duration(days: value.round())),
|
|
),
|
|
),
|
|
showTitles: true,
|
|
interval: 1,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
if (_selectedValue == 'daily') ...[
|
|
SizedBox(
|
|
width: MediaQuery.of(context).size.width * 0.90,
|
|
height: MediaQuery.of(context).size.height * 0.5,
|
|
child: PieChart(
|
|
PieChartData(
|
|
sectionsSpace: 0,
|
|
sections: [
|
|
PieChartSectionData(
|
|
value: _todayCurrent / 60, // Progress
|
|
color: Theme.of(context).colorScheme.primaryFixedDim,
|
|
radius: 60,
|
|
title: (_todayGoal > 0)
|
|
// ignore: lines_longer_than_80_chars
|
|
? '${((_todayCurrent * 100 / 60) / _todayGoal).round()} %'
|
|
: '0 %',
|
|
titleStyle: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
color: Theme.of(context).colorScheme.secondary,
|
|
),
|
|
),
|
|
PieChartSectionData(
|
|
value: (_todayCurrent > 0)
|
|
? (_todayGoal - (_todayCurrent / 60))
|
|
: 1, // Total - progress
|
|
color: Theme.of(context).colorScheme.secondary,
|
|
radius: 60,
|
|
showTitle: _todayCurrent <= 0,
|
|
titleStyle: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
color: Theme.of(context).colorScheme.primaryFixedDim,
|
|
),
|
|
title: '0%',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
SizedBox(
|
|
width: MediaQuery.of(context).size.width * 0.90,
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
'You worked for ${_formatCurrent()}${_getText()}',
|
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
'Out of a total goal of ${_formatTime()}${_getText()}',
|
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
int _daysBetween(DateTime from, DateTime to) {
|
|
final d1 = DateTime(from.year, from.month, from.day);
|
|
final d2 = DateTime(to.year, to.month, to.day);
|
|
return (d2.difference(d1).inHours / 24).round();
|
|
}
|
|
}
|