implement portion/carbs mesurement calculation, add buttons for glucose conversion

This commit is contained in:
spinel 2021-10-24 23:53:44 +02:00
parent 18e5381436
commit 68927522db
5 changed files with 428 additions and 134 deletions

View File

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

View File

@ -135,6 +135,34 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
}
}
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<BolusDetailScreen> {
},
),
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<BolusDetailScreen> {
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<BolusDetailScreen> {
),
)
: 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<BolusDetailScreen> {
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<BolusDetailScreen> {
),
)
: Container(),
glucoseDisplayMode == GlucoseDisplayMode.both ||
glucoseDisplayMode ==
GlucoseDisplayMode.bothForDetail
? IconButton(
onPressed: () => convertBetweenMgPerDlAndMmolPerL(
calculateFrom: GlucoseMeasurement.mgPerDl),
icon: const Icon(Icons.calculate),
)
: Container(),
],
),
],

View File

@ -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<LogMealDetailScreen> {
_carbsRatioAccuracies = Accuracy.fetchAllForCarbsRatio();
}
Future<void> 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<LogMealDetailScreen> {
}
}
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<LogMealDetailScreen> {
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<LogMealDetailScreen> {
getItemValue: (item) => item.objectId,
renderItem: (item) => Text(item.value),
onChanged: (value) {
setState(() {
_meal = value;
});
onSelectMeal(value);
},
),
StyledFutureDropdownButton<MealSource>(
@ -251,43 +334,103 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
});
},
),
// 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<Accuracy>(
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<Accuracy>(
selectedItem: _carbsRatioAccuracy,
@ -328,19 +471,6 @@ class _LogMealDetailScreenState extends State<LogMealDetailScreen> {
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
),
// TODO: autofill the following fields on selecting a source
StyledFutureDropdownButton<Accuracy>(
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(

View File

@ -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<MealDetailScreen> {
final GlobalKey<FormState> _mealForm = GlobalKey<FormState>();
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<MealDetailScreen> {
}
}
Future<void> 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<MealDetailScreen> {
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<MealCategory>(
selectedItem: _category,
label: 'Meal Category',
@ -222,44 +287,103 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
});
},
),
// 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<Accuracy>(
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<Accuracy>(
selectedItem: _carbsRatioAccuracy,
@ -291,19 +415,6 @@ class _MealDetailScreenState extends State<MealDetailScreen> {
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
),
// TODO: autofill the following fields on selecting a source
StyledFutureDropdownButton<Accuracy>(
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(

View File

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