support NutritionalPlan start & end dates

This commit is contained in:
Dieter Plaetinck
2025-06-04 12:49:43 +03:00
parent cad2054610
commit 8049bcf617
3 changed files with 122 additions and 17 deletions

View File

@@ -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"

View File

@@ -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<String, dynamic> json) => _$NutritionalPlanFromJson(json);
factory NutritionalPlan.fromJson(Map<String, dynamic> json) =>
_$NutritionalPlanFromJson(json);
Map<String, dynamic> 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<DateTime, NutritionalValues> get logEntriesValues {
final out = <DateTime, NutritionalValues>{};
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<Log> 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) {

View File

@@ -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<NutritionPlansProvider>(context, listen: false).addMealItem(mealItem, meal);
Provider.of<NutritionPlansProvider>(context, listen: false)
.addMealItem(mealItem, meal);
},
barcode: barcode ?? '',
test: test ?? false,
@@ -235,9 +239,11 @@ class IngredientFormState extends State<IngredientForm> {
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<IngredientForm> {
),
],
),
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<IngredientForm> {
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<PlanForm> {
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<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')}';
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<PlanForm> {
@override
void dispose() {
_descriptionController.dispose();
_startDateController.dispose();
_endDateController.dispose();
colorController.dispose();
super.dispose();
}
@@ -551,6 +569,66 @@ 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
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<PlanForm> {
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 ?? '',