diff --git a/lib/models/meal.dart b/lib/models/meal.dart index 21bc628..90feb1e 100644 --- a/lib/models/meal.dart +++ b/lib/models/meal.dart @@ -1,5 +1,7 @@ import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; +enum PortionCarbsParameter { carbsRatio, portionSize, carbsPerPortion } + class Meal { late String? objectId; late String value; diff --git a/lib/screens/bolus/bolus_detail.dart b/lib/screens/bolus/bolus_detail.dart index ed70a5b..83a37c1 100644 --- a/lib/screens/bolus/bolus_detail.dart +++ b/lib/screens/bolus/bolus_detail.dart @@ -135,6 +135,34 @@ class _BolusDetailScreenState extends State { } } + void convertBetweenMgPerDlAndMmolPerL({GlucoseMeasurement? calculateFrom}) { + // TODO figure out why this isnt happening automatically + int? mgPerDl; + double? mmolPerL; + + if (calculateFrom != GlucoseMeasurement.mmolPerL && + _mgPerDlController.text != '') { + mgPerDl = int.tryParse(_mgPerDlController.text); + } + if (calculateFrom != GlucoseMeasurement.mgPerDl && + _mmolPerLController.text != '') { + mmolPerL = double.tryParse(_mmolPerLController.text); + } + + if (mgPerDl != null && mmolPerL == null) { + setState(() { + _mmolPerLController.text = + Utils.convertMgPerDlToMmolPerL(mgPerDl!).toString(); + }); + } + if (mmolPerL != null && mgPerDl == null) { + setState(() { + _mgPerDlController.text = + Utils.convertMmolPerLToMgPerDl(mmolPerL!).toString(); + }); + } + } + @override Widget build(BuildContext context) { bool isNew = widget.bolus == null; @@ -221,7 +249,6 @@ class _BolusDetailScreenState extends State { }, ), Row( - // TODO: improve conversion of mg/dl and mmol/l children: [ glucoseMeasurement == GlucoseMeasurement.mgPerDl || glucoseDisplayMode == GlucoseDisplayMode.both || @@ -234,16 +261,8 @@ class _BolusDetailScreenState extends State { suffixText: 'mg/dl', ), controller: _mgPerDlController, - onChanged: (_) { - setState(() { - _mmolPerLController - .text = Utils.convertMgPerDlToMmolPerL( - int.tryParse( - _mmolPerLController.text) ?? - 0) - .toString(); - }); - }, + onChanged: (_) => + convertBetweenMgPerDlAndMmolPerL, keyboardType: const TextInputType.numberWithOptions(), validator: (value) { @@ -256,6 +275,15 @@ class _BolusDetailScreenState extends State { ), ) : Container(), + glucoseDisplayMode == GlucoseDisplayMode.both || + glucoseDisplayMode == + GlucoseDisplayMode.bothForDetail + ? IconButton( + onPressed: () => convertBetweenMgPerDlAndMmolPerL( + calculateFrom: GlucoseMeasurement.mmolPerL), + icon: const Icon(Icons.calculate), + ) + : Container(), glucoseMeasurement == GlucoseMeasurement.mmolPerL || glucoseDisplayMode == GlucoseDisplayMode.both || glucoseDisplayMode == @@ -267,16 +295,8 @@ class _BolusDetailScreenState extends State { suffixText: 'mmol/l', ), controller: _mmolPerLController, - onChanged: (_) { - setState(() { - _mgPerDlController - .text = Utils.convertMmolPerLToMgPerDl( - double.tryParse( - _mgPerDlController.text) ?? - 0) - .toString(); - }); - }, + onChanged: (_) => + convertBetweenMgPerDlAndMmolPerL, keyboardType: const TextInputType.numberWithOptions( decimal: true), @@ -290,6 +310,15 @@ class _BolusDetailScreenState extends State { ), ) : Container(), + glucoseDisplayMode == GlucoseDisplayMode.both || + glucoseDisplayMode == + GlucoseDisplayMode.bothForDetail + ? IconButton( + onPressed: () => convertBetweenMgPerDlAndMmolPerL( + calculateFrom: GlucoseMeasurement.mgPerDl), + icon: const Icon(Icons.calculate), + ) + : Container(), ], ), ], diff --git a/lib/screens/log/log_meal_detail.dart b/lib/screens/log/log_meal_detail.dart index e6255f3..d811209 100644 --- a/lib/screens/log/log_meal_detail.dart +++ b/lib/screens/log/log_meal_detail.dart @@ -11,6 +11,7 @@ import 'package:diameter/models/meal_portion_type.dart'; import 'package:diameter/models/meal_source.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/settings.dart'; +import 'package:diameter/utils/utils.dart'; import 'package:flutter/material.dart'; class LogMealDetailScreen extends StatefulWidget { @@ -83,6 +84,49 @@ class _LogMealDetailScreenState extends State { _carbsRatioAccuracies = Accuracy.fetchAllForCarbsRatio(); } + Future onSelectMeal(String? objectId) async { + if (objectId != null) { + Meal? meal = await Meal.get(objectId); + if (meal != null) { + setState(() { + _meal = objectId; + _valueController.text = meal.value; + if (meal.carbsRatio != null) { + _carbsRatioController.text = meal.carbsRatio.toString(); + } + if (meal.portionSize != null) { + _portionSizeController.text = meal.portionSize.toString(); + } + if (meal.carbsPerPortion != null) { + _carbsPerPortionController.text = meal.carbsPerPortion.toString(); + } + if (meal.delayedBolusRate != null) { + _delayedBolusRateController.text = meal.delayedBolusRate.toString(); + } + if (meal.delayedBolusDuration != null) { + _delayedBolusDurationController.text = + meal.delayedBolusDuration.toString(); + } + if (meal.source != null) { + _source = meal.source; + } + if (meal.category != null) { + _category = meal.category; + } + if (meal.portionType != null) { + _portionType = meal.portionType; + } + if (meal.portionSizeAccuracy != null) { + _portionSizeAccuracy = meal.portionSizeAccuracy; + } + if (meal.carbsRatioAccuracy != null) { + _carbsRatioAccuracy = meal.carbsRatioAccuracy; + } + }); + } + } + } + void handleSaveAction() async { if (_logMealForm.currentState!.validate()) { bool isNew = widget.logMeal == null; @@ -175,6 +219,48 @@ class _LogMealDetailScreenState extends State { } } + void calculateThirdMeasurementOfPortionCarbsRelation( + {PortionCarbsParameter? parameterToBeCalculated}) { + double? carbsRatio; + double? portionSize; + double? carbsPerPortion; + + if (parameterToBeCalculated != PortionCarbsParameter.carbsRatio && + _carbsRatioController.text != '') { + carbsRatio = double.tryParse(_carbsRatioController.text); + } + if (parameterToBeCalculated != PortionCarbsParameter.portionSize && + _portionSizeController.text != '') { + portionSize = double.tryParse(_portionSizeController.text); + } + if (parameterToBeCalculated != PortionCarbsParameter.carbsPerPortion && + _carbsRatioController.text != '') { + carbsPerPortion = double.tryParse(_carbsPerPortionController.text); + } + + if (carbsRatio != null && portionSize != null && carbsPerPortion == null) { + setState(() { + _carbsPerPortionController.text = + Utils.calculateCarbsPerPortion(carbsRatio!, portionSize!) + .toString(); + }); + } + if (carbsRatio == null && portionSize != null && carbsPerPortion != null) { + setState(() { + _carbsRatioController.text = + Utils.calculateCarbsRatio(carbsPerPortion!, portionSize!) + .toString(); + }); + } + if (carbsRatio != null && portionSize == null && carbsPerPortion != null) { + setState(() { + _portionSizeController.text = + Utils.calculatePortionSize(carbsRatio!, carbsPerPortion!) + .toString(); + }); + } + } + @override Widget build(BuildContext context) { bool isNew = widget.logMeal == null; @@ -190,7 +276,6 @@ class _LogMealDetailScreenState extends State { StyledForm( formState: _logMealForm, fields: [ - // TODO: autofill all associated fields on selecting a meal TextFormField( controller: _valueController, decoration: const InputDecoration( @@ -210,9 +295,7 @@ class _LogMealDetailScreenState extends State { getItemValue: (item) => item.objectId, renderItem: (item) => Text(item.value), onChanged: (value) { - setState(() { - _meal = value; - }); + onSelectMeal(value); }, ), StyledFutureDropdownButton( @@ -251,43 +334,103 @@ class _LogMealDetailScreenState extends State { }); }, ), - // TODO: if 2 out of the 3 following fields are given, calc 3rd - TextFormField( - decoration: const InputDecoration( - labelText: 'Carbs ratio', - suffixText: '%', - ), - controller: _carbsRatioController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), + Row( + children: [ + Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Carbs ratio', + suffixText: '%', + ), + controller: _carbsRatioController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + onChanged: (_) => + calculateThirdMeasurementOfPortionCarbsRelation(), + ), + ), + IconButton( + onPressed: () => + calculateThirdMeasurementOfPortionCarbsRelation( + parameterToBeCalculated: + PortionCarbsParameter.carbsRatio), + icon: const Icon(Icons.calculate), + ), + ], ), - TextFormField( - decoration: InputDecoration( - labelText: 'Portion size', - suffixText: nutritionMeasurement == - NutritionMeasurement.grams - ? 'g' - : nutritionMeasurement == NutritionMeasurement.ounces - ? 'oz' - : '', - ), - controller: _portionSizeController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: TextFormField( + decoration: InputDecoration( + labelText: 'Portion size', + suffixText: + nutritionMeasurement == NutritionMeasurement.grams + ? 'g' + : nutritionMeasurement == + NutritionMeasurement.ounces + ? 'oz' + : '', + alignLabelWithHint: true, + ), + controller: _portionSizeController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + onChanged: (_) => + calculateThirdMeasurementOfPortionCarbsRelation(), + ), + ), + IconButton( + onPressed: () => + calculateThirdMeasurementOfPortionCarbsRelation( + parameterToBeCalculated: + PortionCarbsParameter.portionSize), + icon: const Icon(Icons.calculate), + ), + ], ), - TextFormField( - decoration: InputDecoration( - labelText: 'Carbs per portion', - suffixText: nutritionMeasurement == - NutritionMeasurement.grams - ? 'g' - : nutritionMeasurement == NutritionMeasurement.ounces - ? 'oz' - : '', - ), - controller: _carbsPerPortionController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), + StyledFutureDropdownButton( + selectedItem: _portionSizeAccuracy, + label: 'Portion Size Accuracy', + items: _portionSizeAccuracies, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _portionSizeAccuracy = value; + }); + }, + ), + Row( + children: [ + Expanded( + child: TextFormField( + decoration: InputDecoration( + labelText: 'Carbs per portion', + suffixText: + nutritionMeasurement == NutritionMeasurement.grams + ? 'g' + : nutritionMeasurement == + NutritionMeasurement.ounces + ? 'oz' + : '', + ), + controller: _carbsPerPortionController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + onChanged: (_) => + calculateThirdMeasurementOfPortionCarbsRelation(), + ), + ), + IconButton( + onPressed: () => + calculateThirdMeasurementOfPortionCarbsRelation( + parameterToBeCalculated: + PortionCarbsParameter.carbsPerPortion), + icon: const Icon(Icons.calculate), + ), + ], ), StyledFutureDropdownButton( selectedItem: _carbsRatioAccuracy, @@ -328,19 +471,6 @@ class _LogMealDetailScreenState extends State { keyboardType: const TextInputType.numberWithOptions(decimal: true), ), - // TODO: autofill the following fields on selecting a source - StyledFutureDropdownButton( - selectedItem: _portionSizeAccuracy, - label: 'Portion Size Accuracy', - items: _portionSizeAccuracies, - getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _portionSizeAccuracy = value; - }); - }, - ), TextFormField( controller: _notesController, decoration: const InputDecoration( diff --git a/lib/screens/meal/meal_detail.dart b/lib/screens/meal/meal_detail.dart index c030675..7f4e9fd 100644 --- a/lib/screens/meal/meal_detail.dart +++ b/lib/screens/meal/meal_detail.dart @@ -9,6 +9,7 @@ import 'package:diameter/models/meal_portion_type.dart'; import 'package:diameter/models/meal_source.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/settings.dart'; +import 'package:diameter/utils/utils.dart'; import 'package:flutter/material.dart'; class MealDetailScreen extends StatefulWidget { @@ -23,13 +24,13 @@ class MealDetailScreen extends StatefulWidget { class _MealDetailScreenState extends State { final GlobalKey _mealForm = GlobalKey(); - final _valueController = TextEditingController(); - final _carbsRatioController = TextEditingController(); - final _portionSizeController = TextEditingController(); - final _carbsPerPortionController = TextEditingController(); - final _delayedBolusRateController = TextEditingController(); - final _delayedBolusDurationController = TextEditingController(); - final _notesController = TextEditingController(); + final _valueController = TextEditingController(text: ''); + final _carbsRatioController = TextEditingController(text: ''); + final _portionSizeController = TextEditingController(text: ''); + final _carbsPerPortionController = TextEditingController(text: ''); + final _delayedBolusRateController = TextEditingController(text: ''); + final _delayedBolusDurationController = TextEditingController(text: ''); + final _notesController = TextEditingController(text: ''); String? _source; String? _category; String? _portionType; @@ -158,6 +159,73 @@ class _MealDetailScreenState extends State { } } + Future onSelectMealSource(String? objectId) async { + if (objectId != null) { + MealSource? mealSource = await MealSource.get(objectId); + if (mealSource != null) { + setState(() { + _source = objectId; + if (mealSource.defaultCarbsRatioAccuracy != null) { + _carbsRatioAccuracy = + mealSource.defaultCarbsRatioAccuracy.toString(); + } + if (mealSource.defaultPortionSizeAccuracy != null) { + _portionSizeAccuracy = + mealSource.defaultPortionSizeAccuracy.toString(); + } + if (mealSource.defaultMealCategory != null) { + _category = mealSource.defaultMealCategory.toString(); + } + if (mealSource.defaultMealPortionType != null) { + _portionType = mealSource.defaultMealPortionType.toString(); + } + }); + } + } + } + + void calculateThirdMeasurementOfPortionCarbsRelation( + {PortionCarbsParameter? parameterToBeCalculated}) { + double? carbsRatio; + double? portionSize; + double? carbsPerPortion; + + if (parameterToBeCalculated != PortionCarbsParameter.carbsRatio && + _carbsRatioController.text != '') { + carbsRatio = double.tryParse(_carbsRatioController.text); + } + if (parameterToBeCalculated != PortionCarbsParameter.portionSize && + _portionSizeController.text != '') { + portionSize = double.tryParse(_portionSizeController.text); + } + if (parameterToBeCalculated != PortionCarbsParameter.carbsPerPortion && + _carbsRatioController.text != '') { + carbsPerPortion = double.tryParse(_carbsPerPortionController.text); + } + + if (carbsRatio != null && portionSize != null && carbsPerPortion == null) { + setState(() { + _carbsPerPortionController.text = + Utils.calculateCarbsPerPortion(carbsRatio!, portionSize!) + .toString(); + }); + } + if (carbsRatio == null && portionSize != null && carbsPerPortion != null) { + setState(() { + _carbsRatioController.text = + Utils.calculateCarbsRatio(carbsPerPortion!, portionSize!) + .toString(); + }); + } + if (carbsRatio != null && portionSize == null && carbsPerPortion != null) { + setState(() { + _portionSizeController.text = + Utils.calculatePortionSize(carbsRatio!, carbsPerPortion!) + .toString(); + }); + } + } + @override Widget build(BuildContext context) { bool isNew = widget.meal == null; @@ -192,12 +260,9 @@ class _MealDetailScreenState extends State { getItemValue: (item) => item.objectId, renderItem: (item) => Text(item.value), onChanged: (value) { - setState(() { - _source = value; - }); + onSelectMealSource(value); }, ), - // TODO: autofill the following fields on selecting a source StyledFutureDropdownButton( selectedItem: _category, label: 'Meal Category', @@ -222,44 +287,103 @@ class _MealDetailScreenState extends State { }); }, ), - // TODO: if 2 out of the 3 following fields are given, calc 3rd - TextFormField( - decoration: const InputDecoration( - labelText: 'Carbs ratio', - suffixText: '%', - ), - controller: _carbsRatioController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), + Row( + children: [ + Expanded( + child: TextFormField( + decoration: const InputDecoration( + labelText: 'Carbs ratio', + suffixText: '%', + ), + controller: _carbsRatioController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + onChanged: (_) => + calculateThirdMeasurementOfPortionCarbsRelation(), + ), + ), + IconButton( + onPressed: () => + calculateThirdMeasurementOfPortionCarbsRelation( + parameterToBeCalculated: + PortionCarbsParameter.carbsRatio), + icon: const Icon(Icons.calculate), + ), + ], ), - TextFormField( - decoration: InputDecoration( - labelText: 'Portion size', - suffixText: nutritionMeasurement == - NutritionMeasurement.grams - ? 'g' - : nutritionMeasurement == NutritionMeasurement.ounces - ? 'oz' - : '', - alignLabelWithHint: true, - ), - controller: _portionSizeController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: TextFormField( + decoration: InputDecoration( + labelText: 'Portion size', + suffixText: + nutritionMeasurement == NutritionMeasurement.grams + ? 'g' + : nutritionMeasurement == + NutritionMeasurement.ounces + ? 'oz' + : '', + alignLabelWithHint: true, + ), + controller: _portionSizeController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + onChanged: (_) => + calculateThirdMeasurementOfPortionCarbsRelation(), + ), + ), + IconButton( + onPressed: () => + calculateThirdMeasurementOfPortionCarbsRelation( + parameterToBeCalculated: + PortionCarbsParameter.portionSize), + icon: const Icon(Icons.calculate), + ), + ], ), - TextFormField( - decoration: InputDecoration( - labelText: 'Carbs per portion', - suffixText: nutritionMeasurement == - NutritionMeasurement.grams - ? 'g' - : nutritionMeasurement == NutritionMeasurement.ounces - ? 'oz' - : '', - ), - controller: _carbsPerPortionController, - keyboardType: - const TextInputType.numberWithOptions(decimal: true), + StyledFutureDropdownButton( + selectedItem: _portionSizeAccuracy, + label: 'Portion Size Accuracy', + items: _portionSizeAccuracies, + getItemValue: (item) => item.objectId, + renderItem: (item) => Text(item.value), + onChanged: (value) { + setState(() { + _portionSizeAccuracy = value; + }); + }, + ), + Row( + children: [ + Expanded( + child: TextFormField( + decoration: InputDecoration( + labelText: 'Carbs per portion', + suffixText: + nutritionMeasurement == NutritionMeasurement.grams + ? 'g' + : nutritionMeasurement == + NutritionMeasurement.ounces + ? 'oz' + : '', + ), + controller: _carbsPerPortionController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + onChanged: (_) => + calculateThirdMeasurementOfPortionCarbsRelation(), + ), + ), + IconButton( + onPressed: () => + calculateThirdMeasurementOfPortionCarbsRelation( + parameterToBeCalculated: + PortionCarbsParameter.carbsPerPortion), + icon: const Icon(Icons.calculate), + ), + ], ), StyledFutureDropdownButton( selectedItem: _carbsRatioAccuracy, @@ -291,19 +415,6 @@ class _MealDetailScreenState extends State { keyboardType: const TextInputType.numberWithOptions(decimal: true), ), - // TODO: autofill the following fields on selecting a source - StyledFutureDropdownButton( - selectedItem: _portionSizeAccuracy, - label: 'Portion Size Accuracy', - items: _portionSizeAccuracies, - getItemValue: (item) => item.objectId, - renderItem: (item) => Text(item.value), - onChanged: (value) { - setState(() { - _portionSizeAccuracy = value; - }); - }, - ), TextFormField( controller: _notesController, decoration: const InputDecoration( diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 680ae69..1e5e6e4 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,9 +1,31 @@ +import 'dart:math'; + class Utils { + static double roundToDecimalPlaces(double value, int places) { + double mod = pow(10.0, places).toDouble(); + return ((value * mod).round().toDouble() / mod); + } + static double convertMgPerDlToMmolPerL(int mgPerDl) { - return (mgPerDl / 18.018).roundToDouble(); + return Utils.roundToDecimalPlaces(mgPerDl * 0.0555, 2); } static int convertMmolPerLToMgPerDl(double mmolPerL) { return (mmolPerL * 18.018).round(); } + + static double calculateCarbsPerPortion( + double carbsRatio, double portionSize) { + return Utils.roundToDecimalPlaces(carbsRatio * portionSize / 100, 2); + } + + static double calculateCarbsRatio( + double carbsPerPortion, double portionSize) { + return Utils.roundToDecimalPlaces(carbsPerPortion * 100 / portionSize, 2); + } + + static double calculatePortionSize( + double carbsRatio, double carbsPerPortion) { + return Utils.roundToDecimalPlaces(carbsPerPortion * 100 / carbsRatio, 2); + } }