Initial (redacted) commit.
This commit is contained in:
commit
655f8a036a
368 changed files with 20949 additions and 0 deletions
76
app/lib/pages/dashboard_hydration_subpage.dart
Normal file
76
app/lib/pages/dashboard_hydration_subpage.dart
Normal file
|
@ -0,0 +1,76 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:habitrack_app/pages/hydration_graph_widget.dart';
|
||||
|
||||
class SubpageHydrationButton extends StatelessWidget {
|
||||
const SubpageHydrationButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 300,
|
||||
margin: const EdgeInsets.only(top: 20, bottom: 7.5),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
minimumSize: const Size(10, 70),
|
||||
),
|
||||
onPressed: () => {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const DashboardHydrationSubpage(),
|
||||
),
|
||||
),
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.local_drink,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Text(
|
||||
' Hydration Widgets',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DashboardHydrationSubpage extends StatefulWidget {
|
||||
const DashboardHydrationSubpage({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DashboardHydrationSubpageState();
|
||||
}
|
||||
|
||||
class _DashboardHydrationSubpageState extends State<DashboardHydrationSubpage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
appBar: AppBar(
|
||||
iconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
title: Text(
|
||||
'Statistics: Hydration Widgets',
|
||||
textScaler: const TextScaler.linear(1.2),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: const HydrationGraphWidget(),
|
||||
);
|
||||
}
|
||||
}
|
47
app/lib/pages/dashboard_page.dart
Normal file
47
app/lib/pages/dashboard_page.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/pages/dashboard_hydration_subpage.dart';
|
||||
import 'package:habitrack_app/pages/dashboard_task_subpage.dart';
|
||||
import 'package:habitrack_app/pages/dashboard_timer_subpage.dart';
|
||||
import 'package:habitrack_app/pages/reset_subpage.dart';
|
||||
|
||||
class DashboardPage extends ConsumerStatefulWidget {
|
||||
const DashboardPage({super.key});
|
||||
@override
|
||||
ConsumerState<DashboardPage> createState() => _DashboardPageState();
|
||||
}
|
||||
|
||||
class _DashboardPageState extends ConsumerState<DashboardPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//final items = ref.watch(itemsProvider);
|
||||
|
||||
//final len = items.value!.length;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'Dashboard',
|
||||
textScaler: const TextScaler.linear(1.4),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
body: ColoredBox(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: const Column(
|
||||
children: [
|
||||
SubpageHydrationButton(),
|
||||
SubpageTaskButton(),
|
||||
SubpageTimerButton(),
|
||||
ResetSubpageButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
76
app/lib/pages/dashboard_task_subpage.dart
Normal file
76
app/lib/pages/dashboard_task_subpage.dart
Normal file
|
@ -0,0 +1,76 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:habitrack_app/pages/tasks_graph_widget.dart';
|
||||
|
||||
class SubpageTaskButton extends StatelessWidget {
|
||||
const SubpageTaskButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 300,
|
||||
margin: const EdgeInsets.only(top: 7.5, bottom: 7.5),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
minimumSize: const Size(10, 70),
|
||||
),
|
||||
onPressed: () => {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const DashboardTaskSubpage(),
|
||||
),
|
||||
),
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.task,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Text(
|
||||
' Task Widgets',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DashboardTaskSubpage extends StatefulWidget {
|
||||
const DashboardTaskSubpage({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DashboardTaskSubpageState();
|
||||
}
|
||||
|
||||
class _DashboardTaskSubpageState extends State<DashboardTaskSubpage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
appBar: AppBar(
|
||||
iconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
title: Text(
|
||||
'Statistics: Tasks Widgets',
|
||||
textScaler: const TextScaler.linear(1.2),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: TasksGraphWidget(),
|
||||
);
|
||||
}
|
||||
}
|
76
app/lib/pages/dashboard_timer_subpage.dart
Normal file
76
app/lib/pages/dashboard_timer_subpage.dart
Normal file
|
@ -0,0 +1,76 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:habitrack_app/pages/timer_graph_widget.dart';
|
||||
|
||||
class SubpageTimerButton extends StatelessWidget {
|
||||
const SubpageTimerButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 300,
|
||||
margin: const EdgeInsets.only(top: 7.5, bottom: 7.5),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
minimumSize: const Size(10, 70),
|
||||
),
|
||||
onPressed: () => {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const DashboardTimerSubpage(),
|
||||
),
|
||||
),
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.timer,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Text(
|
||||
' Timer Widgets',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DashboardTimerSubpage extends StatefulWidget {
|
||||
const DashboardTimerSubpage({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DashboardTimerSubpageState();
|
||||
}
|
||||
|
||||
class _DashboardTimerSubpageState extends State<DashboardTimerSubpage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
appBar: AppBar(
|
||||
iconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
title: Text(
|
||||
'Statistics: Timer Widgets',
|
||||
textScaler: const TextScaler.linear(1.2),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: TimerGraphWidget(),
|
||||
);
|
||||
}
|
||||
}
|
478
app/lib/pages/hydration_graph_widget.dart
Normal file
478
app/lib/pages/hydration_graph_widget.dart
Normal file
|
@ -0,0 +1,478 @@
|
|||
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_state.dart';
|
||||
import 'package:habitrack_app/main.dart';
|
||||
import 'package:habitrack_app/sembast/hydration.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DataPoint {
|
||||
DataPoint({required this.date, required this.progress});
|
||||
DateTime date;
|
||||
double progress;
|
||||
}
|
||||
|
||||
class HydrationGraphWidget extends ConsumerStatefulWidget {
|
||||
const HydrationGraphWidget({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||
_TasksGraphWidgetState();
|
||||
}
|
||||
|
||||
class _TasksGraphWidgetState extends ConsumerState<HydrationGraphWidget> {
|
||||
void _buildList(List<dynamic> value) {
|
||||
final items = value;
|
||||
_thisWeekCompleted = 0;
|
||||
_thisWeekPlanned = 0;
|
||||
_maxAmount = 0;
|
||||
|
||||
setState(() {
|
||||
_todayCompleted = 0;
|
||||
_todayPlanned = 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);
|
||||
}
|
||||
});
|
||||
_thisWeekPlanned = 0;
|
||||
|
||||
for (final item in items) {
|
||||
if (_selectedValue == 'weekly') {
|
||||
var alreadyAdded = false;
|
||||
for (var i = 0; i <= 6; i++) {
|
||||
DateTime parsedDate;
|
||||
|
||||
if (item is Hydration) {
|
||||
if (item.current / item.goal >= 1 && item.completedOn != '') {
|
||||
//item is completed
|
||||
parsedDate = DateTime.parse(item.completedOn);
|
||||
} else {
|
||||
parsedDate = DateTime.parse(item.createdOn);
|
||||
logger.i('GOAL: $item.goal');
|
||||
}
|
||||
|
||||
logger
|
||||
.i('BEFORE $i days ago and AFTER ${i + 1} days ago, element at '
|
||||
'index ${6 - i} will be updated.');
|
||||
|
||||
if (parsedDate.isBefore(_latestDate.subtract(Duration(days: i))) &&
|
||||
parsedDate
|
||||
.isAfter(_latestDate.subtract(Duration(days: i + 1)))) {
|
||||
logger.i('LOOPING');
|
||||
if (!alreadyAdded) {
|
||||
alreadyAdded = true;
|
||||
_thisWeekCompleted += item.current / 1000;
|
||||
|
||||
_thisWeekPlanned += item.goal / 1000;
|
||||
}
|
||||
|
||||
if (item.goal > _maxAmount) {
|
||||
_maxAmount = (item.goal.toDouble() / 1000).ceilToDouble();
|
||||
}
|
||||
if (item.current > _maxAmount && item.current > item.goal) {
|
||||
_maxAmount = (item.current.toDouble() / 1000).ceilToDouble();
|
||||
}
|
||||
// Update maxAmount
|
||||
setState(() {
|
||||
_weeklyPlannedEntries.elementAt(6 - i).progress =
|
||||
item.goal.toDouble() / 1000;
|
||||
_weeklyWorkedEntries.elementAt(6 - i).progress =
|
||||
item.current.toDouble() / 1000;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (_selectedValue == 'daily' && item is Hydration) {
|
||||
logger.i('DATES');
|
||||
final parsedDate = DateTime.parse(item.createdOn);
|
||||
logger.i('LATEST DATE: $_latestDate');
|
||||
|
||||
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');
|
||||
_todayCompleted = item.current / 1000;
|
||||
_todayPlanned = item.goal / 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showPreviousWeek() {
|
||||
setState(() {
|
||||
_todayPlanned = 0;
|
||||
_todayCompleted = 0;
|
||||
_thisWeekPlanned = 0;
|
||||
_thisWeekCompleted = 0;
|
||||
});
|
||||
|
||||
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() {
|
||||
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);
|
||||
|
||||
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';
|
||||
|
||||
double _thisWeekCompleted = 0;
|
||||
double _thisWeekPlanned = 0;
|
||||
double _maxAmount = 0;
|
||||
DateTime _latestDate = DateTime.now();
|
||||
double _todayCompleted = 0;
|
||||
double _todayPlanned = 0;
|
||||
|
||||
List<DataPoint> _weeklyPlannedEntries = <DataPoint>[];
|
||||
List<DataPoint> _weeklyWorkedEntries = <DataPoint>[];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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?');
|
||||
}
|
||||
|
||||
final firstDate = _weeklyPlannedEntries.elementAtOrNull(0)!.date;
|
||||
logger.i('HMMM $firstDate');
|
||||
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 gradientColors = [
|
||||
Colors.cyan,
|
||||
Colors.blueAccent,
|
||||
];
|
||||
final gradient2Colors = [
|
||||
Colors.amber,
|
||||
Colors.amberAccent,
|
||||
];
|
||||
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: _maxAmount,
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
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: 30,
|
||||
getTitlesWidget: (value, meta) => Text(
|
||||
'$value l',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
// interval: 0.25,
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
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,
|
||||
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
|
||||
'${(_selectedValue == 'weekly') ? _thisWeekCompleted : _todayCompleted} liters drunk${_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') ? _thisWeekPlanned : _todayPlanned} liters',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
123
app/lib/pages/reset_subpage.dart
Normal file
123
app/lib/pages/reset_subpage.dart
Normal file
|
@ -0,0 +1,123 @@
|
|||
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/sembast/hydration.dart';
|
||||
import 'package:habitrack_app/sembast/tasks_list.dart';
|
||||
import 'package:habitrack_app/sembast/timer.dart';
|
||||
|
||||
class ResetSubpageButton extends ConsumerStatefulWidget {
|
||||
const ResetSubpageButton({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ResetSubpageButton> createState() => _ResetSubpageButtonState();
|
||||
}
|
||||
|
||||
class _ResetSubpageButtonState extends ConsumerState<ResetSubpageButton> {
|
||||
Future<void> _confirmPopup() async {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
title: Text(
|
||||
'Are you sure?',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 25,
|
||||
),
|
||||
),
|
||||
scrollable: true,
|
||||
actions: <Widget>[
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
final items = ref.watch(itemsProvider);
|
||||
switch (items) {
|
||||
case AsyncData(:final value):
|
||||
final items = value;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
final item = items.elementAt(i);
|
||||
if (item is Hydration) {
|
||||
controller.delete(item.id);
|
||||
} else if (item is TasksItem) {
|
||||
controller.delete(item.id);
|
||||
} else if (item is TimerItem) {
|
||||
controller.delete(item.id);
|
||||
}
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
//DateTime due = DateTime.parse(formattedDate);
|
||||
// ignore: cascade_invocations
|
||||
},
|
||||
child: Text(
|
||||
'Yes',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 300,
|
||||
margin: const EdgeInsets.only(top: 7.5, bottom: 7.5),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
minimumSize: const Size(10, 70),
|
||||
),
|
||||
onPressed: _confirmPopup,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.delete,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Text(
|
||||
'Clear Database',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
687
app/lib/pages/tasks_graph_widget.dart
Normal file
687
app/lib/pages/tasks_graph_widget.dart
Normal file
|
@ -0,0 +1,687 @@
|
|||
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();
|
||||
}
|
||||
}
|
487
app/lib/pages/timer_graph_widget.dart
Normal file
487
app/lib/pages/timer_graph_widget.dart
Normal file
|
@ -0,0 +1,487 @@
|
|||
// 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();
|
||||
}
|
||||
}
|
11
app/lib/pages/widget_page.dart
Normal file
11
app/lib/pages/widget_page.dart
Normal file
|
@ -0,0 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/widget_wall.dart';
|
||||
|
||||
class WidgetPage extends StatelessWidget {
|
||||
const WidgetPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const WidgetWall();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue