diff --git a/lib/models/nutrition/nutritional_plan.dart b/lib/models/nutrition/nutritional_plan.dart index 054acb27..65ac4206 100644 --- a/lib/models/nutrition/nutritional_plan.dart +++ b/lib/models/nutrition/nutritional_plan.dart @@ -19,6 +19,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:logging/logging.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/json.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; @@ -32,6 +33,8 @@ part 'nutritional_plan.g.dart'; @JsonSerializable(explicitToJson: true) class NutritionalPlan { + final _logger = Logger('NutritionalPlan Model'); + @JsonKey(required: true) int? id; @@ -88,6 +91,14 @@ class NutritionalPlan { }) : creationDate = creationDate ?? DateTime.now() { this.meals = meals ?? []; this.diaryEntries = diaryEntries ?? []; + + if (endDate != null && endDate!.isBefore(startDate)) { + _logger.warning( + 'The end date of a nutritional plan is before the start. Setting to null! ' + 'PlanId: $id, startDate: $startDate, endDate: $endDate', + ); + endDate = null; + } } NutritionalPlan.empty() { diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 1e967292..0c4c347e 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -500,27 +500,13 @@ class PlanForm extends StatefulWidget { class _PlanFormState extends State { final _form = GlobalKey(); - bool _onlyLogging = true; GoalType _goalType = GoalType.meals; - - final _descriptionController = TextEditingController(); - final _startDateController = TextEditingController(); - final _endDateController = TextEditingController(); - final TextEditingController colorController = TextEditingController(); - GoalType? selectedGoal; @override void initState() { super.initState(); - _onlyLogging = widget._plan.onlyLogging; - _descriptionController.text = widget._plan.description; - _startDateController.text = dateToYYYYMMDD(widget._plan.startDate)!; - // ignore invalid enddates should the server gives us one - if (widget._plan.endDate != null && widget._plan.endDate!.isAfter(widget._plan.startDate)) { - _endDateController.text = dateToYYYYMMDD(widget._plan.endDate)!; - } if (widget._plan.hasAnyAdvancedGoals) { _goalType = GoalType.advanced; } else if (widget._plan.hasAnyGoals) { @@ -530,17 +516,10 @@ class _PlanFormState extends State { } } - @override - void dispose() { - _descriptionController.dispose(); - _startDateController.dispose(); - _endDateController.dispose(); - colorController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { + final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode); + return Form( key: _form, child: ListView( @@ -551,7 +530,9 @@ class _PlanFormState extends State { decoration: InputDecoration( labelText: AppLocalizations.of(context).description, ), - controller: _descriptionController, + controller: TextEditingController( + text: widget._plan.description, + ), onSaved: (newValue) { widget._plan.description = newValue!; }, @@ -560,10 +541,15 @@ class _PlanFormState extends State { TextFormField( key: const Key('field-start-date'), decoration: InputDecoration( - labelText: AppLocalizations.of(context).start, - hintText: 'YYYY-MM-DD', + labelText: AppLocalizations.of(context).startDate, + suffixIcon: const Icon( + Icons.calendar_today, + key: Key('calendarIcon'), + ), + ), + controller: TextEditingController( + text: dateFormat.format(widget._plan.startDate), ), - controller: _startDateController, readOnly: true, onTap: () async { // Stop keyboard from appearing @@ -579,11 +565,18 @@ class _PlanFormState extends State { if (pickedDate != null) { setState(() { - _startDateController.text = dateToYYYYMMDD(pickedDate)!; widget._plan.startDate = pickedDate; }); } }, + validator: (value) { + if (widget._plan.endDate != null && + widget._plan.endDate!.isBefore(widget._plan.startDate)) { + return 'End date must be after start date'; + } + + return null; + }, ), // End Date Row( @@ -593,11 +586,28 @@ class _PlanFormState extends State { 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', + suffixIcon: widget._plan.endDate == null + ? const Icon( + Icons.calendar_today, + key: Key('calendarIcon'), + ) + : IconButton( + icon: const Icon(Icons.clear), + tooltip: 'Clear end date', + onPressed: () { + setState(() { + widget._plan.endDate = null; + }); + }, + ), + ), + controller: TextEditingController( + text: widget._plan.endDate == null + ? '' + : dateFormat.format(widget._plan.endDate!), ), - controller: _endDateController, readOnly: true, onTap: () async { // Stop keyboard from appearing @@ -606,47 +616,30 @@ class _PlanFormState extends State { // 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 + initialDate: widget._plan.endDate, + // end must be after start + firstDate: widget._plan.startDate.add(const Duration(days: 1)), lastDate: DateTime(2100), ); if (pickedDate != null) { setState(() { - _endDateController.text = dateToYYYYMMDD(pickedDate)!; 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), - value: _onlyLogging, + value: widget._plan.onlyLogging, onChanged: (value) { setState(() { - _onlyLogging = !_onlyLogging; + widget._plan.onlyLogging = value; }); - widget._plan.onlyLogging = value; }, ), Row( @@ -658,7 +651,7 @@ class _PlanFormState extends State { const SizedBox(width: 8), Expanded( child: DropdownButtonFormField( - value: _goalType, + initialValue: _goalType, items: GoalType.values .map( (e) => DropdownMenuItem( @@ -766,9 +759,6 @@ class _PlanFormState extends State { ); } } - - // Saving was successful, reset the data - _descriptionController.clear(); }, ), ], diff --git a/lib/widgets/routines/forms/routine.dart b/lib/widgets/routines/forms/routine.dart index 03b5a10a..b2c679e9 100644 --- a/lib/widgets/routines/forms/routine.dart +++ b/lib/widgets/routines/forms/routine.dart @@ -110,7 +110,7 @@ class _RoutineFormState extends State { }, decoration: InputDecoration( labelText: i18n.startDate, - suffixIcon: Icon( + suffixIcon: const Icon( Icons.calendar_today, key: Key('calendarIcon'), ), diff --git a/test/nutrition/nutritional_plan_form_test.dart b/test/nutrition/nutritional_plan_form_test.dart index 2fc96495..5b67fdf6 100644 --- a/test/nutrition/nutritional_plan_form_test.dart +++ b/test/nutrition/nutritional_plan_form_test.dart @@ -38,6 +38,7 @@ void main() { id: 1, creationDate: DateTime(2021, 1, 1), startDate: DateTime(2021, 1, 1), + endDate: DateTime(2021, 2, 10), description: 'test plan 1', ); final plan2 = NutritionalPlan.empty(); @@ -80,11 +81,10 @@ void main() { await tester.pumpWidget(createHomeScreen(plan1)); await tester.pumpAndSettle(); - expect( - find.text('test plan 1'), - findsOneWidget, - reason: 'Description of existing nutritional plan is filled in', - ); + expect(find.text('test plan 1'), findsOneWidget, reason: 'Description is filled in'); + expect(find.text('1/1/2021'), findsOneWidget, reason: 'Start date is filled in'); + expect(find.text('2/10/2021'), findsOneWidget, reason: 'End date is filled in'); + await tester.enterText(find.byKey(const Key('field-description')), 'New description'); await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME)));