From 8049bcf61721a6dd6c673dcc4fc2a71540107097 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Wed, 4 Jun 2025 12:49:43 +0300 Subject: [PATCH] support NutritionalPlan start & end dates --- lib/l10n/app_en.arb | 8 ++ lib/models/nutrition/nutritional_plan.dart | 29 ++++-- lib/widgets/nutrition/forms.dart | 102 ++++++++++++++++++--- 3 files changed, 122 insertions(+), 17 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f61691be..2e976b4e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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" diff --git a/lib/models/nutrition/nutritional_plan.dart b/lib/models/nutrition/nutritional_plan.dart index 4318f07e..afee8f2e 100644 --- a/lib/models/nutrition/nutritional_plan.dart +++ b/lib/models/nutrition/nutritional_plan.dart @@ -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; @@ -84,6 +90,8 @@ class NutritionalPlan { NutritionalPlan.empty() { creationDate = DateTime.now(); + startDate = DateTime.now(); + endDate = null; description = ''; onlyLogging = false; goalEnergy = null; @@ -94,12 +102,15 @@ class NutritionalPlan { } // Boilerplate - factory NutritionalPlan.fromJson(Map json) => _$NutritionalPlanFromJson(json); + factory NutritionalPlan.fromJson(Map json) => + _$NutritionalPlanFromJson(json); Map toJson() => _$NutritionalPlanToJson(this); String getLabel(BuildContext context) { - return description != '' ? description : AppLocalizations.of(context).nutritionalPlan; + return description != '' + ? description + : AppLocalizations.of(context).nutritionalPlan; } bool get hasAnyGoals { @@ -154,7 +165,9 @@ class NutritionalPlan { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); - return logEntriesValues.containsKey(today) ? logEntriesValues[today]! : NutritionalValues(); + return logEntriesValues.containsKey(today) + ? logEntriesValues[today]! + : NutritionalValues(); } NutritionalValues get loggedNutritionalValues7DayAvg { @@ -170,7 +183,8 @@ class NutritionalPlan { Map get logEntriesValues { final out = {}; for (final log in diaryEntries) { - final date = DateTime(log.datetime.year, log.datetime.month, log.datetime.day); + final date = + DateTime(log.datetime.year, log.datetime.month, log.datetime.day); if (!out.containsKey(date)) { out[date] = NutritionalValues(); @@ -195,7 +209,8 @@ class NutritionalPlan { final List out = []; for (final log in diaryEntries) { final dateKey = DateTime(date.year, date.month, date.day); - final logKey = DateTime(log.datetime.year, log.datetime.month, log.datetime.day); + final logKey = + DateTime(log.datetime.year, log.datetime.month, log.datetime.day); if (dateKey == logKey) { out.add(log); @@ -212,7 +227,9 @@ class NutritionalPlan { for (final meal in meals) { for (final mealItem in meal.mealItems) { final found = out.firstWhereOrNull( - (e) => e.amount == mealItem.amount && e.ingredientId == mealItem.ingredientId, + (e) => + e.amount == mealItem.amount && + e.ingredientId == mealItem.ingredientId, ); if (found == null) { diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 18d5740e..4b560990 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -42,7 +42,8 @@ class MealForm extends StatelessWidget { final _nameController = TextEditingController(); MealForm(this._planId, [meal]) { - _meal = meal ?? Meal(plan: _planId, time: TimeOfDay.fromDateTime(DateTime.now())); + _meal = meal ?? + Meal(plan: _planId, time: TimeOfDay.fromDateTime(DateTime.now())); _timeController.text = timeToString(_meal.time)!; _nameController.text = _meal.name; } @@ -57,7 +58,8 @@ class MealForm extends StatelessWidget { children: [ TextFormField( key: const Key('field-time'), - decoration: InputDecoration(labelText: AppLocalizations.of(context).time), + decoration: + InputDecoration(labelText: AppLocalizations.of(context).time), controller: _timeController, onTap: () async { // Stop keyboard from appearing @@ -79,7 +81,8 @@ class MealForm extends StatelessWidget { TextFormField( maxLength: 25, key: const Key('field-name'), - decoration: InputDecoration(labelText: AppLocalizations.of(context).name), + decoration: + InputDecoration(labelText: AppLocalizations.of(context).name), controller: _nameController, onSaved: (newValue) { _meal.name = newValue as String; @@ -125,7 +128,8 @@ Widget MealItemForm( recent: recent.map((e) => Log.fromMealItem(e, 0, e.mealId)).toList(), onSave: (BuildContext context, MealItem mealItem, DateTime? dt) { mealItem.mealId = meal.id!; - Provider.of(context, listen: false).addMealItem(mealItem, meal); + Provider.of(context, listen: false) + .addMealItem(mealItem, meal); }, barcode: barcode ?? '', test: test ?? false, @@ -235,9 +239,11 @@ class IngredientFormState extends State { Widget build(BuildContext context) { final String unit = AppLocalizations.of(context).g; final queryLower = _searchQuery.toLowerCase(); - final suggestions = - widget.recent.where((e) => e.ingredient.name.toLowerCase().contains(queryLower)).toList(); - final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString()); + final suggestions = widget.recent + .where((e) => e.ingredient.name.toLowerCase().contains(queryLower)) + .toList(); + final numberFormat = + NumberFormat.decimalPattern(Localizations.localeOf(context).toString()); return Container( margin: const EdgeInsets.all(20), @@ -344,7 +350,8 @@ class IngredientFormState extends State { ), ], ), - if (ingredientIdController.text.isNotEmpty && _amountController.text.isNotEmpty) + if (ingredientIdController.text.isNotEmpty && + _amountController.text.isNotEmpty) Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -395,7 +402,8 @@ class IngredientFormState extends State { return; } _form.currentState!.save(); - _mealItem.ingredientId = int.parse(_ingredientIdController.text); + _mealItem.ingredientId = + int.parse(_ingredientIdController.text); var date = DateTime.parse(_dateController.text); final tod = stringToTime(_timeController.text); @@ -508,6 +516,8 @@ class _PlanFormState extends State { GoalType _goalType = GoalType.meals; final _descriptionController = TextEditingController(); + final _startDateController = TextEditingController(); + final _endDateController = TextEditingController(); final TextEditingController colorController = TextEditingController(); GoalType? selectedGoal; @@ -518,6 +528,12 @@ class _PlanFormState extends State { _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')}'; + if (widget._plan.endDate != null) { + _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) { @@ -530,6 +546,8 @@ class _PlanFormState extends State { @override void dispose() { _descriptionController.dispose(); + _startDateController.dispose(); + _endDateController.dispose(); colorController.dispose(); super.dispose(); } @@ -551,6 +569,66 @@ class _PlanFormState extends State { 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 + TextFormField( + key: const Key('field-end-date'), + decoration: InputDecoration( + labelText: AppLocalizations.of(context).endDate, + hintText: 'YYYY-MM-DD', + ), + controller: _endDateController, + 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.endDate, + firstDate: DateTime(2000), + 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; + }); + } + }, + ), SwitchListTile( title: Text(AppLocalizations.of(context).onlyLogging), subtitle: Text(AppLocalizations.of(context).onlyLoggingHelpText), @@ -626,7 +704,8 @@ class _PlanFormState extends State { val: widget._plan.goalCarbohydrates?.toString(), label: AppLocalizations.of(context).goalCarbohydrates, suffix: AppLocalizations.of(context).g, - onSave: (double value) => widget._plan.goalCarbohydrates = value, + onSave: (double value) => + widget._plan.goalCarbohydrates = value, key: const Key('field-goal-carbohydrates'), ), GoalMacros( @@ -706,7 +785,8 @@ class GoalMacros extends StatelessWidget { @override Widget build(BuildContext context) { - final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString()); + final numberFormat = + NumberFormat.decimalPattern(Localizations.localeOf(context).toString()); return TextFormField( initialValue: val ?? '',