Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8049bcf
support NutritionalPlan start & end dates
Dieterbe Jun 4, 2025
f18a60b
dart run build_runner build
Dieterbe Jun 27, 2025
eabe425
show start & end dates in the relevant places
Dieterbe Jun 27, 2025
7986df3
consistent style
Dieterbe Jun 27, 2025
d1cfce7
adjust older code for 'current plan' and showing weight during plan
Dieterbe Jun 27, 2025
56adac2
display weight change per NP on the NP list page
Dieterbe Jun 27, 2025
8ee1195
update tests for new start&end fields
Dieterbe Jun 27, 2025
450b4da
fix
Dieterbe Jun 27, 2025
1e5b44c
fix tests part 1
Dieterbe Jun 27, 2025
a0e2659
dart format --line-length=100 .
Dieterbe Jun 27, 2025
650ef38
simpler date formatting
Dieterbe Jun 28, 2025
20713ee
fix tests
Dieterbe Jun 28, 2025
e382387
fix localisation for 'open ended'
Dieterbe Jun 28, 2025
68d9a1f
update goldens
Dieterbe Jun 28, 2025
40f94c2
cleanup
Dieterbe Jun 28, 2025
29f6c87
show charts for all nutritional plans + more elegant filtering
Dieterbe Jun 28, 2025
d4a272c
use interpolation for higher accuracy
Dieterbe Jun 28, 2025
40dbb76
fix tests
Dieterbe Jun 28, 2025
ac043ba
Merge branch 'master' into nutrition-plan-stats
rolandgeider Sep 3, 2025
4dff6ea
Fix import so that "isSameDayAs" becomes available again
rolandgeider Sep 4, 2025
0fd2af0
Merge branch 'refs/heads/master' into nutrition-plan-stats
rolandgeider Sep 12, 2025
6cc9631
Simplify currentPlan
rolandgeider Sep 12, 2025
d9b4d66
Add tests for interpolation logic
rolandgeider Sep 12, 2025
dbd3fa9
Refactor PlanForm
rolandgeider Sep 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,14 @@
"@date": {
"description": "The date of a workout log or body weight entry"
},
"creationDate": "Start date",
"@creationDate": {
"description": "The Start date of a nutritional plan"
},
"endDate": "End date",
"@endDate": {
"description": "The End date of a nutritional plan"
},
"value": "Value",
"@value": {
"description": "The value of a measurement entry"
Expand Down
10 changes: 10 additions & 0 deletions lib/models/nutrition/nutritional_plan.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ class NutritionalPlan {
@JsonKey(required: true, name: 'creation_date', toJson: dateToYYYYMMDD)
late DateTime creationDate;

@JsonKey(required: true, name: 'start', toJson: dateToYYYYMMDD)
late DateTime startDate;

@JsonKey(required: true, name: 'end', toJson: dateToYYYYMMDD)
late DateTime? endDate;

@JsonKey(required: true, name: 'only_logging')
late bool onlyLogging;

Expand Down Expand Up @@ -69,6 +75,8 @@ class NutritionalPlan {
this.id,
required this.description,
required this.creationDate,
required this.startDate,
this.endDate,
this.onlyLogging = false,
this.goalEnergy,
this.goalProtein,
Expand All @@ -84,6 +92,8 @@ class NutritionalPlan {

NutritionalPlan.empty() {
creationDate = DateTime.now();
startDate = DateTime.now();
endDate = null;
description = '';
onlyLogging = false;
goalEnergy = null;
Expand Down
6 changes: 6 additions & 0 deletions lib/models/nutrition/nutritional_plan.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 22 additions & 5 deletions lib/providers/nutrition.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,30 @@ class NutritionPlansProvider with ChangeNotifier {
ingredients = [];
}

/// Returns the current active nutritional plan. At the moment this is just
/// the latest, but this might change in the future.
/// Returns the current active nutritional plan.
/// A plan is considered active if:
/// - Its start date is before now
/// - Its end date is after now or not set
/// If multiple plans match these criteria, the one with the most recent creation date is returned.
NutritionalPlan? get currentPlan {
if (_plans.isNotEmpty) {
return _plans.first;
if (_plans.isEmpty) {
return null;
}
return null;

final now = DateTime.now();
final activePlans = _plans.where((plan) {
final isAfterStart = plan.startDate.isBefore(now);
final isBeforeEnd = plan.endDate == null || plan.endDate!.isAfter(now);
return isAfterStart && isBeforeEnd;
}).toList();

if (activePlans.isEmpty) {
return null;
}

// Sort by creation date (newest first) and return the first one
activePlans.sort((a, b) => b.creationDate.compareTo(a.creationDate));
return activePlans.first;
}

NutritionalPlan findById(int id) {
Expand Down
25 changes: 20 additions & 5 deletions lib/widgets/measurements/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ List<Widget> getOverviewWidgets(
];
}

// TODO(dieter): i'm not sure if this handles well the case where weights were not logged consistently
// e.g. if the plan runs for a month, but the first point is after 3 weeks.
// and the last (non-included) point was *right* before the startDate.
// wouldn't it be better to interpolate the missing points?
List<Widget> getOverviewWidgetsSeries(
String name,
List<MeasurementChartEntry> entriesAll,
Expand All @@ -35,7 +39,10 @@ List<Widget> getOverviewWidgetsSeries(
BuildContext context,
) {
final monthAgo = DateTime.now().subtract(const Duration(days: 30));
final showPlan = plan != null && entriesAll.any((e) => e.date.isAfter(plan.creationDate));
final showPlan = plan != null &&
entriesAll.any((e) =>
e.date.isAfter(plan.startDate) &&
(plan.endDate == null || e.date.isBefore(plan.endDate!)));

return [
...getOverviewWidgets(
Expand All @@ -48,8 +55,16 @@ List<Widget> getOverviewWidgetsSeries(
if (showPlan)
...getOverviewWidgets(
AppLocalizations.of(context).chartDuringPlanTitle(name, plan.description),
entriesAll.where((e) => e.date.isAfter(plan.creationDate)).toList(),
entries7dAvg.where((e) => e.date.isAfter(plan.creationDate)).toList(),
entriesAll
.where((e) =>
e.date.isAfter(plan.startDate) &&
(plan.endDate == null || e.date.isBefore(plan.endDate!)))
.toList(),
entries7dAvg
.where((e) =>
e.date.isAfter(plan.startDate) &&
(plan.endDate == null || e.date.isBefore(plan.endDate!)))
.toList(),
unit,
context,
),
Expand All @@ -62,7 +77,7 @@ List<Widget> getOverviewWidgetsSeries(
(plan == null ||
(showPlan &&
entriesAll
.firstWhere((e) => e.date.isAfter(plan.creationDate))
.firstWhere((e) => e.date.isAfter(plan.startDate))
.date
.isBefore(entriesAll.last.date.subtract(const Duration(days: 30))))) &&
entriesAll.any((e) => e.date.isAfter(monthAgo)))
Expand Down Expand Up @@ -92,7 +107,7 @@ List<Widget> getOverviewWidgetsSeries(
];
}

// return the raw and average meaasurements for a "sensible range"
// return the raw and average measurements for a "sensible range"
// a sensible range is something relatively recent, which is most relevant
// for the user to track their progress, but a range should always include
// at least 5 points, and if not we chose a bigger one.
Expand Down
95 changes: 95 additions & 0 deletions lib/widgets/nutrition/forms.dart
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@ class _PlanFormState extends State<PlanForm> {
GoalType _goalType = GoalType.meals;

final _descriptionController = TextEditingController();
final _startDateController = TextEditingController();
final _endDateController = TextEditingController();
final TextEditingController colorController = TextEditingController();

GoalType? selectedGoal;
Expand All @@ -518,6 +520,13 @@ class _PlanFormState extends State<PlanForm> {

_onlyLogging = widget._plan.onlyLogging;
_descriptionController.text = widget._plan.description;
_startDateController.text =
'${widget._plan.startDate.year}-${widget._plan.startDate.month.toString().padLeft(2, '0')}-${widget._plan.startDate.day.toString().padLeft(2, '0')}';
// ignore invalid enddates should the server gives us one
if (widget._plan.endDate != null && widget._plan.endDate!.isAfter(widget._plan.startDate)) {
_endDateController.text =
'${widget._plan.endDate!.year}-${widget._plan.endDate!.month.toString().padLeft(2, '0')}-${widget._plan.endDate!.day.toString().padLeft(2, '0')}';
}
if (widget._plan.hasAnyAdvancedGoals) {
_goalType = GoalType.advanced;
} else if (widget._plan.hasAnyGoals) {
Expand All @@ -530,6 +539,8 @@ class _PlanFormState extends State<PlanForm> {
@override
void dispose() {
_descriptionController.dispose();
_startDateController.dispose();
_endDateController.dispose();
colorController.dispose();
super.dispose();
}
Expand All @@ -551,6 +562,90 @@ class _PlanFormState extends State<PlanForm> {
widget._plan.description = newValue!;
},
),
// Start Date
TextFormField(
key: const Key('field-start-date'),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).start,
hintText: 'YYYY-MM-DD',
),
controller: _startDateController,
readOnly: true,
onTap: () async {
// Stop keyboard from appearing
FocusScope.of(context).requestFocus(FocusNode());

// Open date picker
final pickedDate = await showDatePicker(
context: context,
initialDate: widget._plan.startDate,
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);

if (pickedDate != null) {
setState(() {
_startDateController.text =
'${pickedDate.year}-${pickedDate.month.toString().padLeft(2, '0')}-${pickedDate.day.toString().padLeft(2, '0')}';
widget._plan.startDate = pickedDate;
});
}
},
),
// End Date
Row(
children: [
Expanded(
child: TextFormField(
key: const Key('field-end-date'),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).endDate,
hintText: 'YYYY-MM-DD',
helperText:
'Tip: only for athletes with contest deadlines. Most users benefit from flexibility',
),
controller: _endDateController,
readOnly: true,
onTap: () async {
// Stop keyboard from appearing
FocusScope.of(context).requestFocus(FocusNode());

// Open date picker
final pickedDate = await showDatePicker(
context: context,
// if somehow the server has an invalid end date, default to null
initialDate: (widget._plan.endDate != null &&
widget._plan.endDate!.isAfter(widget._plan.startDate))
? widget._plan.endDate!
: null,
firstDate: widget._plan.startDate
.add(const Duration(days: 1)), // end must be after start
lastDate: DateTime(2100),
);

if (pickedDate != null) {
setState(() {
_endDateController.text =
'${pickedDate.year}-${pickedDate.month.toString().padLeft(2, '0')}-${pickedDate.day.toString().padLeft(2, '0')}';
widget._plan.endDate = pickedDate;
});
}
},
),
),
if (_endDateController.text.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear),
tooltip: 'Clear end date',
onPressed: () {
setState(() {
_endDateController.text = '';
widget._plan.endDate = null;
});
},
),
],
),
SwitchListTile(
title: Text(AppLocalizations.of(context).onlyLogging),
subtitle: Text(AppLocalizations.of(context).onlyLoggingHelpText),
Expand Down
21 changes: 21 additions & 0 deletions lib/widgets/nutrition/nutritional_plan_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/nutrition/nutritional_plan.dart';
Expand All @@ -42,6 +43,26 @@ class NutritionalPlanDetailWidget extends StatelessWidget {
return SliverList(
delegate: SliverChildListDelegate(
[
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
_nutritionalPlan.endDate != null
? 'from ${DateFormat.yMd(
Localizations.localeOf(context).languageCode,
).format(_nutritionalPlan.startDate)} to ${DateFormat.yMd(
Localizations.localeOf(context).languageCode,
).format(_nutritionalPlan.endDate!)}'
: 'from ${DateFormat.yMd(
Localizations.localeOf(context).languageCode,
).format(_nutritionalPlan.startDate)} (open ended)',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontStyle: FontStyle.italic,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 8),
SizedBox(
width: 300,
child: Padding(
Expand Down
Loading
Loading