habitrack/app/lib/pages/tasks_graph_widget.dart
2024-08-26 00:34:20 +02:00

687 lines
23 KiB
Dart

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/tasks_list.dart';
import 'package:intl/intl.dart';
class DataPoint {
DataPoint({required this.date, required this.progress});
DateTime date;
double progress;
}
class TasksGraphWidget extends ConsumerStatefulWidget {
TasksGraphWidget({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() =>
_TasksGraphWidgetState();
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 _TasksGraphWidgetState extends ConsumerState<TasksGraphWidget> {
void _buildList(List<dynamic> value) {
final items = value;
_tasksCompleted = 0;
_tasksPlanned = 0;
_weeklyOverdue = 0;
_totalTasksCompleted = 0;
_totalTasksPlanned = 0;
_maxTasks = 0;
_weeklyPlannedEntries = [];
_weeklyOverdueEntries = [];
_todayOverdue = 0;
_totalOverdue = 0;
const seperator = '_SEPARATOR_';
setState(() {
_todayCompleted = 0;
_todayPlanned = 0;
});
for (var i = 0; i <= 6; i++) {
final itemToInsert = DataPoint(
date: _latestDate.subtract(Duration(days: 6 - i)),
progress: 0,
);
_weeklyPlannedEntries.add(itemToInsert);
}
_weeklyWorkedEntries = [];
for (var i = 0; i <= 6; i++) {
final itemToInsert = DataPoint(
date: _latestDate.subtract(Duration(days: 6 - i)),
progress: 0,
);
_weeklyWorkedEntries.add(itemToInsert);
}
for (var i = 0; i <= 6; i++) {
final itemToInsert = DataPoint(
date: _latestDate.subtract(Duration(days: 6 - i)),
progress: 0,
);
_weeklyOverdueEntries.add(itemToInsert);
}
for (final item in items) {
if (_selectedValue == 'weekly' && item is TasksItem) {
logger.i('HMM');
_tasksCompleted = 0;
for (final individualToDo in item.completedTaskList) {
final toConvert = individualToDo.split(seperator);
final parsedDate = DateTime.parse(toConvert.elementAtOrNull(2)!);
var alreadyAdded = false;
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');
if (!alreadyAdded) {
alreadyAdded = true;
_tasksCompleted += 1;
_totalTasksCompleted += 1;
}
// Update maxAmount
setState(() {
logger.i('COMPLETED: $_tasksCompleted');
_weeklyWorkedEntries.elementAt(6 - i).progress =
_tasksCompleted.toDouble();
_weeklyPlannedEntries.elementAt(6 - i).progress =
_tasksPlanned.toDouble() +
_tasksCompleted.toDouble() +
_weeklyOverdue;
});
}
}
}
_tasksPlanned = 0;
_weeklyOverdue = 0;
for (final individualToDo in item.taskList) {
final toConvert = individualToDo.split(seperator);
final due = DateTime.parse(toConvert.elementAtOrNull(1)!);
final completedOn = DateTime.parse(toConvert.elementAtOrNull(3)!);
final parsedDate = completedOn;
var alreadyAdded = false;
logger.i('NANI');
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');
// Update maxAmount
setState(() {
if (!alreadyAdded) {
alreadyAdded = true;
final now = DateTime.now();
// _maxTasks += 1;
if (DateTime(now.year, now.month, now.day).isAfter(due)) {
logger
..i('OVERDUE TASK')
..i('NOW: $now')
..i('DUE: $due');
_totalOverdue += 1;
_weeklyOverdue += 1;
_weeklyOverdueEntries.elementAt(6 - i).progress =
_weeklyOverdue;
} else {
logger.i('Task is NOT overdue');
_tasksPlanned += 1;
_totalTasksPlanned += 1;
}
}
_weeklyPlannedEntries.elementAt(6 - i).progress =
_tasksPlanned.toDouble() +
_tasksCompleted.toDouble() +
_weeklyOverdue;
});
}
}
logger
..i('TASKS PLANNED FOR THIS DAY: $_tasksPlanned')
..i('TASKS COMPLETED FOR THIS DAY: $_tasksCompleted')
..i('TOTAL TASKS: ${_tasksPlanned + _tasksCompleted}');
if (_tasksPlanned + _tasksCompleted + _weeklyOverdue > _maxTasks) {
_maxTasks = (_tasksPlanned + _tasksCompleted + _weeklyOverdue)
.ceilToDouble();
logger.i('MAX TASKS: $_maxTasks');
}
}
} else if (_selectedValue == 'daily' && item is TasksItem) {
for (final individualToDo in item.completedTaskList) {
final toConvert = individualToDo.split(seperator);
final completedOn = DateTime.parse(toConvert.elementAtOrNull(3)!);
final completed =
toConvert.elementAtOrNull(4)!.toLowerCase() == 'true';
final oneDayAgo = _latestDate.subtract(const Duration(days: 1));
final parsedDate = completedOn;
if (parsedDate.isBefore(oneDayAgo)) {
logger.i('More than a day old');
} else if (parsedDate.isAfter(oneDayAgo) &&
parsedDate.isBefore(_latestDate)) {
logger.i('TOday');
if (completed) {
_todayCompleted += 1;
_todayPlanned += 1;
} else {
_todayPlanned += 1;
}
}
}
for (final individualToDo in item.taskList) {
final toConvert = individualToDo.split(seperator);
final due = DateTime.parse(toConvert.elementAtOrNull(1)!);
final createdOn = DateTime.parse(toConvert.elementAtOrNull(2)!);
final completed =
toConvert.elementAtOrNull(4)!.toLowerCase() == 'true';
final oneDayAgo = _latestDate.subtract(const Duration(days: 1));
final parsedDate = createdOn;
if (parsedDate.isBefore(oneDayAgo)) {
logger.i('More than a day old');
} else if (parsedDate.isAfter(oneDayAgo) &&
parsedDate.isBefore(_latestDate)) {
logger.i('TOday');
if (completed) {
_todayCompleted += 1;
_todayPlanned += 1;
} else {
_todayPlanned += 1;
final now = DateTime.now();
if (DateTime(now.year, now.month, now.day).isAfter(due)) {
logger
..i('OVERDUE TASK')
..i('NOW: $now')
..i('DUE: $due');
_totalOverdue += 1;
_todayOverdue += 1;
}
}
}
}
}
}
}
void _showPreviousWeek() {
if (_selectedValue == 'weekly') {
_latestDate = _latestDate.subtract(const Duration(days: 7));
} else if (_selectedValue == 'daily') {
_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() {
if (_selectedValue == 'weekly') {
_latestDate = _latestDate.add(const Duration(days: 7));
} else if (_selectedValue == 'daily') {
_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';
}
final gradientColors = [
Colors.cyan,
Colors.blueAccent,
];
final gradient2Colors = [
Colors.amber,
Colors.amberAccent,
];
final gradient3Colors = [
Colors.deepPurple,
Colors.deepPurpleAccent,
];
List<dynamic> thisWeekItems = [];
List<dynamic> todayItems = [];
String? _selectedValue = 'weekly';
int _tasksCompleted = 0;
int _tasksPlanned = 0;
int _totalTasksCompleted = 0;
int _totalTasksPlanned = 0;
double _maxTasks = 0;
DateTime _latestDate = DateTime.now();
double _todayCompleted = 0;
double _todayPlanned = 0;
double _todayOverdue = 0;
double _weeklyOverdue = 0;
double _totalOverdue = 0;
List<DataPoint> _weeklyPlannedEntries = <DataPoint>[];
List<DataPoint> _weeklyWorkedEntries = <DataPoint>[];
List<DataPoint> _weeklyOverdueEntries = <DataPoint>[];
@override
Widget build(BuildContext context) {
final items = ref.watch(itemsProvider);
// ignore: unused_element
double getProgress() {
if (_todayPlanned == 0) {
return 0;
}
logger.i(
'GLORIOUS PROGRESS${_todayCompleted / _todayPlanned}',
);
return _todayCompleted; // - this._todayCompleted;
}
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.elementAtOrNull(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.elementAt(0).date, dataPoint.date)
.toDouble();
final lcbd = FlSpot(xValue, dataPoint.progress);
weeklyWorkedSpots.add(lcbd);
}
final weeklyOverdueSpots = <FlSpot>[];
for (final dataPoint in _weeklyOverdueEntries) {
final xValue = _daysBetween(_weeklyOverdueEntries[0].date, dataPoint.date)
.toDouble();
final lcbd = FlSpot(xValue, dataPoint.progress);
weeklyOverdueSpots.add(lcbd);
// 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(
// color: Theme.of(context).colorScheme.primaryContainer,
padding:
const EdgeInsets.only(bottom: 15, top: 15, right: 15, left: 5),
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
height: MediaQuery.of(context).size.height * 0.5,
child: LineChart(
LineChartData(
minY: 0,
maxY: _maxTasks,
lineBarsData: [
LineChartBarData(
isCurved: true,
preventCurveOverShooting: true,
spots: flSpots,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: gradientColors
.map((color) => color.withOpacity(0.6))
.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.6))
.toList(),
),
),
),
LineChartBarData(
color: Colors.purple,
isCurved: true,
isStrokeCapRound: true,
preventCurveOverShooting: true,
spots: weeklyOverdueSpots,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: gradient3Colors
.map((color) => color.withOpacity(0.6))
.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.toInt()} task${value == 1 ? '' : 's'}',
style: const TextStyle(
fontSize: 10,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.bold,
),
),
interval: 1,
),
),
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: _todayCompleted, // Progress
color: Theme.of(context).colorScheme.primaryFixedDim,
// Cyan
radius: 60,
title: (_todayPlanned != 0)
// ignore: lines_longer_than_80_chars
? '${((_todayCompleted / _todayPlanned) * 100).floorToDouble()} %'
: '0 %',
titleStyle: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.secondary,
),
),
PieChartSectionData(
titleStyle: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primaryFixedDim,
),
value: (_todayPlanned > 0 && _todayCompleted > 0)
? (_todayPlanned - _todayCompleted)
: 1, //
// Total - progress
color: Theme.of(context).colorScheme.secondary,
radius: 60,
showTitle: _todayPlanned == 0 ||
(_todayPlanned > 0 && _todayCompleted == 0),
title: '0 %',
),
],
),
),
),
],
SizedBox(
width: MediaQuery.of(context).size.width * 0.90,
child: Column(
children: [
Text(
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontSize: 15,
fontWeight: FontWeight.bold,
),
// ignore: lines_longer_than_80_chars
_getCompletedDescription() + _getText(),
),
Text(
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontSize: 15,
fontWeight: FontWeight.bold,
),
// ignore: lines_longer_than_80_chars
'Out of a total goal of ${(_selectedValue == 'weekly') ? (_totalTasksPlanned + _totalTasksCompleted + _totalOverdue).toInt() : _todayPlanned.toInt()}${_getText()}',
),
Text(
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontSize: 15,
fontWeight: FontWeight.bold,
),
_getMissedDescription(),
),
],
),
),
],
);
}
String _getCompletedDescription() {
if (_selectedValue == 'weekly') {
if (_totalTasksCompleted == 0) {
return '0 tasks completed';
} else if (_totalTasksCompleted == 1) {
return '1 task completed';
} else {
return '$_totalTasksCompleted tasks completed';
}
} else if (_selectedValue == 'daily') {
if (_todayCompleted == 0) {
return '0 tasks completed';
} else if (_todayCompleted == 1) {
return '1 task completed';
} else {
return '${_todayCompleted.toInt()} tasks completed';
}
}
return '';
}
String _getMissedDescription() {
if (_selectedValue == 'weekly') {
if (_totalOverdue == 0) {
return 'No tasks were missed';
} else if (_totalOverdue == 1) {
return '1 task was missed';
} else {
return '${_totalOverdue.toInt()} tasks were missed';
}
} else if (_selectedValue == 'daily') {
if (_todayOverdue == 0) {
return 'No tasks were missed';
} else if (_todayOverdue == 1) {
return '1 task was missed';
} else {
return '${_todayOverdue.toInt()} tasks were missed';
}
}
return '';
}
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();
}
}