nutrition plans list: show weekly delta and total duration, consistent styling

This commit is contained in:
Dieter Plaetinck
2025-09-27 12:06:56 +02:00
parent 78d5b3017c
commit 18bb7ca5c8
2 changed files with 65 additions and 10 deletions

View File

@@ -30,6 +30,51 @@ List<DateTime> daysInRange(DateTime first, DateTime last) {
);
}
/// Formats a Duration into a human-readable string with years, months, and days
String humanDuration(DateTime startDate, DateTime endDate) {
var years = endDate.year - startDate.year;
var months = endDate.month - startDate.month;
var days = endDate.day - startDate.day;
if (months < 0) {
months += 12;
years -= 1;
}
// if we overcounted the days, it's a bit trickier to solve than overcounting months
// e.g. consider a start date february 10 and end date june 8
// -> days = -2
// -> months = 4
// the proper answer can be thought of in 3 ways:
// * count whole months first, then days: from febr 10 to may 10, and then to june 8. that's 3 months + (num_days_in_may - 10 + 8 days)
// * count days first, then whole months: from febr 10 to march 8, and then to june 8. that's (num_days_in_feb - 10 + 8 days) + 3 months
// * technically, one can also use any month to adjust the day: e.g.
// count months from febr 10 to april 10, count days to may 8, and count months again to june 8.
// probably no-one uses the last method. The first approach seems most natural.
// it means we need to know the number of days (aka the last day index) of the month before endDate
// which we can do by setting the day to 0 and asking for the day
if (days < 0) {
days += DateTime(endDate.year, endDate.month, 0).day;
months -= 1;
}
final parts = <String>[];
if (years > 0) {
parts.add('$years year${years == 1 ? '' : 's'}');
}
if (months > 0) {
parts.add('$months month${months == 1 ? '' : 's'}');
}
if (days > 0) {
parts.add('$days day${days == 1 ? '' : 's'}');
}
if (parts.isEmpty) {
return 'Duration: 0 days';
}
return 'Duration: ${parts.join(', ')}';
}
extension DateTimeExtension on DateTime {
bool isSameDayAs(DateTime other) {
final thisDay = DateTime(year, month, day);

View File

@@ -19,6 +19,7 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:wger/helpers/date.dart';
import 'package:wger/helpers/measurements.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/body_weight.dart';
@@ -48,8 +49,14 @@ class NutritionalPlansList extends StatelessWidget {
final lastWeight = entries7dAvg.last;
final weightDifference = lastWeight.value - firstWeight.value;
// Calculate the time period in weeks
final timeDifference = lastWeight.date.difference(firstWeight.date);
final weeklyRate =
weightDifference / (timeDifference.inDays == 0 ? 1 : timeDifference.inDays) * 7;
// Format the weight change text and determine color
final String weightChangeText;
final String weeklyRateText;
final Color weightChangeColor;
final profile = context.read<UserProvider>().profile;
@@ -57,12 +64,15 @@ class NutritionalPlansList extends StatelessWidget {
if (weightDifference > 0) {
weightChangeText = '+${weightDifference.toStringAsFixed(1)} $unit';
weeklyRateText = '+${weeklyRate.toStringAsFixed(2)} $unit';
weightChangeColor = Colors.red;
} else if (weightDifference < 0) {
weightChangeText = '${weightDifference.toStringAsFixed(1)} $unit';
weeklyRateText = '${weeklyRate.toStringAsFixed(2)} $unit';
weightChangeColor = Colors.green;
} else {
weightChangeText = '0 $unit';
weeklyRateText = '0 $unit';
weightChangeColor = Colors.grey;
}
@@ -71,15 +81,11 @@ class NutritionalPlansList extends StatelessWidget {
child: Row(
children: [
Text(
'${AppLocalizations.of(context).weight} change: ',
style: Theme.of(context).textTheme.bodySmall,
'${AppLocalizations.of(context).weight}: ',
),
Text(
weightChangeText,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.bold,
color: weightChangeColor,
),
'$weightChangeText ($weeklyRateText/week)',
style: TextStyle(color: weightChangeColor),
),
],
),
@@ -109,14 +115,18 @@ class NutritionalPlansList extends StatelessWidget {
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
humanDuration(
currentPlan.startDate, currentPlan.endDate ?? DateTime.now()),
),
Text(
currentPlan.endDate != null
? 'from ${DateFormat.yMd(
? 'From: ${DateFormat.yMd(
Localizations.localeOf(context).languageCode,
).format(currentPlan.startDate)} to ${DateFormat.yMd(
).format(currentPlan.startDate)} To: ${DateFormat.yMd(
Localizations.localeOf(context).languageCode,
).format(currentPlan.endDate!)}'
: 'from ${DateFormat.yMd(
: 'From: ${DateFormat.yMd(
Localizations.localeOf(context).languageCode,
).format(currentPlan.startDate)} (open ended)',
),