diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index b8089f9e..64784fef 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -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) { @@ -109,7 +126,8 @@ class NutritionPlansProvider with ChangeNotifier { /// Fetches and sets all plans fully, i.e. with all corresponding child objects Future fetchAndSetAllPlansFull() async { - final data = await baseProvider.fetchPaginated(baseProvider.makeUrl(_nutritionalPlansPath)); + final data = await baseProvider + .fetchPaginated(baseProvider.makeUrl(_nutritionalPlansPath)); await Future.wait(data.map((e) => fetchAndSetPlanFull(e['id'])).toList()); } @@ -170,7 +188,8 @@ class NutritionPlansProvider with ChangeNotifier { // Logs await fetchAndSetLogs(plan); for (final meal in meals) { - meal.diaryEntries = plan.diaryEntries.where((e) => e.mealId == meal.id).toList(); + meal.diaryEntries = + plan.diaryEntries.where((e) => e.mealId == meal.id).toList(); } // ... and done @@ -204,7 +223,8 @@ class NutritionPlansProvider with ChangeNotifier { _plans.removeAt(existingPlanIndex); notifyListeners(); - final response = await baseProvider.deleteRequest(_nutritionalPlansPath, id); + final response = + await baseProvider.deleteRequest(_nutritionalPlansPath, id); if (response.statusCode >= 400) { _plans.insert(existingPlanIndex, existingPlan); @@ -284,7 +304,8 @@ class NutritionPlansProvider with ChangeNotifier { notifyListeners(); // Try to delete - final response = await baseProvider.deleteRequest(_mealItemPath, mealItem.id!); + final response = + await baseProvider.deleteRequest(_mealItemPath, mealItem.id!); if (response.statusCode >= 400) { meal.mealItems.insert(mealItemIndex, existingMealItem); notifyListeners(); @@ -299,7 +320,8 @@ class NutritionPlansProvider with ChangeNotifier { /// Fetch and return an ingredient /// /// If the ingredient is not known locally, it is fetched from the server - Future fetchIngredient(int ingredientId, {IngredientDatabase? database}) async { + Future fetchIngredient(int ingredientId, + {IngredientDatabase? database}) async { database ??= this.database; Ingredient ingredient; @@ -317,9 +339,11 @@ class NutritionPlansProvider with ChangeNotifier { _logger.info("Loaded ingredient '${ingredient.name}' from db cache"); // Prune old entries - if (DateTime.now() - .isAfter(ingredientDb.lastFetched.add(const Duration(days: DAYS_TO_CACHE)))) { - (database.delete(database.ingredients)..where((i) => i.id.equals(ingredientId))).go(); + if (DateTime.now().isAfter(ingredientDb.lastFetched + .add(const Duration(days: DAYS_TO_CACHE)))) { + (database.delete(database.ingredients) + ..where((i) => i.id.equals(ingredientId))) + .go(); } } else { final data = await baseProvider.fetch( @@ -347,7 +371,9 @@ class NutritionPlansProvider with ChangeNotifier { final ingredientDb = await database.select(database.ingredients).get(); _logger.info('Read ${ingredientDb.length} ingredients from db cache'); if (ingredientDb.isNotEmpty) { - ingredients = ingredientDb.map((e) => Ingredient.fromJson(jsonDecode(e.data))).toList(); + ingredients = ingredientDb + .map((e) => Ingredient.fromJson(jsonDecode(e.data))) + .toList(); } } diff --git a/lib/widgets/measurements/helpers.dart b/lib/widgets/measurements/helpers.dart index 3881b3a7..0dd6f78e 100644 --- a/lib/widgets/measurements/helpers.dart +++ b/lib/widgets/measurements/helpers.dart @@ -21,11 +21,16 @@ List getOverviewWidgets( height: 220, child: MeasurementChartWidgetFl(raw, unit, avgs: avg), ), - if (avg.isNotEmpty) MeasurementOverallChangeWidget(avg.first, avg.last, unit), + if (avg.isNotEmpty) + MeasurementOverallChangeWidget(avg.first, avg.last, unit), const SizedBox(height: 8), ]; } +// 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 getOverviewWidgetsSeries( String name, List entriesAll, @@ -35,7 +40,10 @@ List 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( @@ -47,9 +55,18 @@ List 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(), + AppLocalizations.of(context) + .chartDuringPlanTitle(name, plan.description), + 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, ), @@ -58,13 +75,15 @@ List getOverviewWidgetsSeries( // then let's show a separate chart just focusing on the last 30 days, // if there is data for it. if (entriesAll.isNotEmpty && - entriesAll.first.date.isBefore(entriesAll.last.date.subtract(const Duration(days: 75))) && + entriesAll.first.date.isBefore( + entriesAll.last.date.subtract(const Duration(days: 75))) && (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))))) && + .isBefore(entriesAll.last.date + .subtract(const Duration(days: 30))))) && entriesAll.any((e) => e.date.isAfter(monthAgo))) ...getOverviewWidgets( AppLocalizations.of(context).chart30DaysTitle(name), @@ -92,7 +111,7 @@ List 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.