Refactor PlanForm

* Date values are now localized
* Remove text controllers since we were setting the values in the plan
  object directly as well as setting them in the controllers anyway. Note
  that this is still not an ideal solution since if we change something
  in the form and close it, the changes are still reflected in the UI, just
  not preserved to the server.
 * Move basic date sanity cheks to the model
This commit is contained in:
Roland Geider
2025-09-12 16:24:41 +02:00
parent d9b4d66c0f
commit dbd3fa915d
4 changed files with 63 additions and 62 deletions

View File

@@ -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() {

View File

@@ -500,27 +500,13 @@ class PlanForm extends StatefulWidget {
class _PlanFormState extends State<PlanForm> {
final _form = GlobalKey<FormState>();
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<PlanForm> {
}
}
@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<PlanForm> {
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<PlanForm> {
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<PlanForm> {
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<PlanForm> {
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<PlanForm> {
// 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<PlanForm> {
const SizedBox(width: 8),
Expanded(
child: DropdownButtonFormField<GoalType>(
value: _goalType,
initialValue: _goalType,
items: GoalType.values
.map(
(e) => DropdownMenuItem<GoalType>(
@@ -766,9 +759,6 @@ class _PlanFormState extends State<PlanForm> {
);
}
}
// Saving was successful, reset the data
_descriptionController.clear();
},
),
],

View File

@@ -110,7 +110,7 @@ class _RoutineFormState extends State<RoutineForm> {
},
decoration: InputDecoration(
labelText: i18n.startDate,
suffixIcon: Icon(
suffixIcon: const Icon(
Icons.calendar_today,
key: Key('calendarIcon'),
),

View File

@@ -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)));