diff --git a/TODO b/TODO index 043b8b7..a7fccd9 100644 --- a/TODO +++ b/TODO @@ -1,30 +1,16 @@ MAIN TASKS: Components/Framework: - ☐ update number fields to use corresponding components - ☐ meal detail (carbs ratio, portion size, carbs per portion) - ☐ log meal detail (amount, carbs ratio, portion size, carbs per portion) - ☐ add "set manually" switch (like in log bolus detail) wherever parameters can be calculated from others - ☐ meal detail - ☐ log meal detail ☐ come up with new concept for duration component ☐ update duration fields to use corresponding component ☐ log event type detail (reminder duration) ☐ log event detail (reminder duration) ☐ meal (bolus delay) ☐ log bolus (delay) - ☐ put dropdowns first if they override name field ☐ set name properties as unique (and add checks to forms) ☐ check through all detail forms and set required fields/according messages ☐ change placement of delete and floating button because its very easy to accidentally hit delete ☐ implement deletion by swiping left on item instead? ☐ check for changes before navigating as well (not just on cancel) - - Log Overview: - ☐ only show current day - ☐ add calendar field on top to navigate - ☐ use currently selected day when adding a log entry - Event Types: - ☐ add pagination Reports: ☐ evaluate what type of reports there should be ☐ try out graph/diagram components @@ -73,6 +59,19 @@ FUTURE TASKS: ☐ add functionality to delete dead records (meaning: set deleted flag and no relations to undeleted records) Archive: + ✔ only show current day @done(22-01-24 05:39) @project(MAIN TASKS.Log Overview) + ✔ add calendar field on top to navigate @done(22-01-24 05:39) @project(MAIN TASKS.Log Overview) + ✔ use currently selected day when adding a log entry @done(22-01-24 05:39) @project(MAIN TASKS.Log Overview) + ✔ only show current day @done(22-01-24 05:39) @project(MAIN TASKS.Event Types) + ✔ add calendar field on top to navigate @done(22-01-24 05:39) @project(MAIN TASKS.Event Types) + ✔ use currently selected day when adding a log event @done(22-01-24 05:39) @project(MAIN TASKS.Event Types) + ✔ update number fields to use corresponding components @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework) + ✔ meal detail (carbs ratio, portion size, carbs per portion) @done(22-01-24 03:12) @project(MAIN TASKS.Components/Framework) + ✔ log meal detail (amount, carbs ratio, portion size, carbs per portion) @done(22-01-24 03:12) @project(MAIN TASKS.Components/Framework) + ✔ add "set manually" switch (like in log bolus detail) wherever parameters can be calculated from others @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework) + ✔ meal detail @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework) + ✔ log meal detail @done(22-01-24 03:13) @project(MAIN TASKS.Components/Framework) + ✔ put dropdowns first if they override name field @done(22-01-24 03:17) @project(MAIN TASKS.Components/Framework) ✔ settings (target glucose, increments) @done(22-01-22 01:48) @project(MAIN TASKS.Components/Framework) ✔ accuracy detail (confidence rating) @done(22-01-21 16:51) @project(MAIN TASKS.Components/Framework) ✔ basal detail (units) @done(22-01-21 18:14) @project(MAIN TASKS.Components/Framework) diff --git a/lib/components/forms/number_form_field.dart b/lib/components/forms/number_form_field.dart index 42e16ab..9b41cb1 100644 --- a/lib/components/forms/number_form_field.dart +++ b/lib/components/forms/number_form_field.dart @@ -75,8 +75,8 @@ class _NumberFormFieldState extends State { children: [ widget.showSteppers ? RepeatOnHoldButton( - onTap: onDecrease, - child: IconButton( + onTap: onDecrease, + child: IconButton( onPressed: double.tryParse(widget.controller.text) != null && (double.parse(widget.controller.text) - widget.step >= widget.min) @@ -84,7 +84,7 @@ class _NumberFormFieldState extends State { : null, icon: const Icon(Icons.remove), ), - ) + ) : Container(), Expanded( child: TextFormField( @@ -100,16 +100,8 @@ class _NumberFormFieldState extends State { onChanged: (input) async { await Future.delayed(const Duration(seconds: 1)); double? value = double.tryParse(input); - if (value != null && - widget.autoRoundToMultipleOfStep && - (value % widget.step != 0)) { - double remainder = value % widget.step; - value = - Utils.addDoublesWithPrecision(value, -remainder, precision); - if (remainder > widget.step / 2) { - value = Utils.addDoublesWithPrecision( - value, widget.step, precision); - } + if (widget.autoRoundToMultipleOfStep) { + value = value != null ? Utils.roundToMultipleOfBase(value, widget.step) : null; } widget.onChanged(value); }, @@ -118,8 +110,8 @@ class _NumberFormFieldState extends State { ), widget.showSteppers ? RepeatOnHoldButton( - onTap: onIncrease, - child: IconButton( + onTap: onIncrease, + child: IconButton( onPressed: double.tryParse(widget.controller.text) != null && (widget.max == null || double.parse(widget.controller.text) + diff --git a/lib/models/log_entry.dart b/lib/models/log_entry.dart index f8f2e4e..3891aeb 100644 --- a/lib/models/log_entry.dart +++ b/lib/models/log_entry.dart @@ -70,6 +70,16 @@ class LogEntry { return dateMap; } + static List getAllForDate(DateTime date) { + DateTime startOfDay = DateTime(date.year, date.month, date.day); + DateTime endOfDay = startOfDay.add(const Duration(days: 1)); + QueryBuilder builder = box.query(LogEntry_.deleted.equals(false)) + ..order(LogEntry_.time, flags: Order.descending); + return builder.build().find().where((entry) { + return (entry.time.compareTo(startOfDay) >= 0 && entry.time.isBefore(endOfDay)); + }).toList(); + } + @override String toString() { return DateTimeUtils.displayDateTime(time); diff --git a/lib/models/log_event.dart b/lib/models/log_event.dart index 2caac05..d35d5f3 100644 --- a/lib/models/log_event.dart +++ b/lib/models/log_event.dart @@ -140,6 +140,54 @@ class LogEvent { return sortedDateMap; } + static List getAllForDate(DateTime date) { + DateTime startOfDay = DateTime(date.year, date.month, date.day); + DateTime endOfDay = startOfDay.add(const Duration(days: 1)); + + List events = []; + + QueryBuilder allByDate = box + .query(LogEvent_.deleted.equals(false)) + ..order(LogEvent_.time, flags: Order.descending); + List startEvents = allByDate.build().find().where((event) { + return (event.time.compareTo(startOfDay) >= 0 && + event.time.isBefore(endOfDay)); + }).toList(); + + for (LogEvent event in startEvents) { + date = DateTime.utc(event.time.year, event.time.month, event.time.day); + LogEvent startEvent = event; + startEvent.title = + '${event.toString()} ${event.hasEndTime ? '(Start)' : ''}'; + events.add(startEvent); + } + + QueryBuilder allByEndDate = box + .query(LogEvent_.deleted.equals(false).and(LogEvent_.endTime.notNull())) + ..order(LogEvent_.endTime, flags: Order.descending); + List endEvents = allByEndDate.build().find().where((event) { + return (event.endTime!.compareTo(startOfDay) >= 0 && + event.endTime!.isBefore(endOfDay)); + }).toList(); + + for (LogEvent event in endEvents) { + date = DateTime.utc( + event.endTime!.year, event.endTime!.month, event.endTime!.day); + LogEvent endEvent = event; + endEvent.isEndEvent = true; + endEvent.title = '${event.toString()} (End)'; + events.add(endEvent); + } + + events.sort((LogEvent a, LogEvent b) { + final dateA = a.isEndEvent ? a.endTime : a.time; + final dateB = b.isEndEvent ? b.endTime : b.time; + return -(dateA!.compareTo(dateB!)); + }); + + return events; + } + @override String toString() { return eventType.target?.value ?? ''; diff --git a/lib/screens/log/log.dart b/lib/screens/log/log.dart index 4ef5bb1..c1d9b5e 100644 --- a/lib/screens/log/log.dart +++ b/lib/screens/log/log.dart @@ -19,25 +19,33 @@ class LogScreen extends StatefulWidget { } class _LogScreenState extends State { - late Map> _logEntryDailyMap; + late List _logEntries; final ScrollController _scrollController = ScrollController(); + final TextEditingController _dateController = TextEditingController(text: ''); + + late DateTime _date; @override void initState() { super.initState(); + + _date = DateTime.now(); + _dateController.text = DateTimeUtils.displayDate(_date); + reload(); } @override void dispose() { _scrollController.dispose(); + _dateController.dispose(); super.dispose(); } void reload({String? message}) { setState(() { - _logEntryDailyMap = LogEntry.getDailyEntryMap(); + _logEntries = LogEntry.getAllForDate(_date); }); setState(() { if (message != null) { @@ -69,216 +77,250 @@ class _LogScreenState extends State { } } + void onChangeDate(DateTime? date) { + if (date != null) { + setState(() { + _date = DateTime(date.year, date.month, date.day); + _dateController.text = DateTimeUtils.displayDate(date); + }); + reload(); + } + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Log Entries'), actions: [ + IconButton( + onPressed: () => onChangeDate(DateTime.now()), + icon: const Icon(Icons.today)), IconButton(onPressed: reload, icon: const Icon(Icons.refresh)), ], ), drawer: const Navigation(currentLocation: LogScreen.routeName), body: Column( children: [ + Row( + children: [ + IconButton( + onPressed: _date.isAtSameMomentAs(DateTime(2000, 1, 1)) + ? null + : () => + onChangeDate(_date.subtract(const Duration(days: 1))), + icon: const Icon(Icons.arrow_back), + ), + Expanded( + child: GestureDetector( + onTap: () async { + final newTime = await showDatePicker( + context: context, + initialDate: _date, + firstDate: DateTime(2000, 1, 1), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + onChangeDate(newTime); + }, + child: Expanded( + child: Text( + DateTimeUtils.displayDate(_date).toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, + textAlign: TextAlign.center, + ), + ), + ), + ), + IconButton( + onPressed: + _date.add(const Duration(days: 1)).isBefore(DateTime.now()) + ? () => onChangeDate(_date.add(const Duration(days: 1))) + : null, + icon: const Icon(Icons.arrow_forward), + ), + ], + ), Expanded( - child: _logEntryDailyMap.isNotEmpty + child: _logEntries.isNotEmpty ? Scrollbar( controller: _scrollController, child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.all(10.0), shrinkWrap: true, - itemCount: _logEntryDailyMap.length, - itemBuilder: (context, dateIndex) { - List dateList = - _logEntryDailyMap.keys.toList(); - final date = dateList[dateIndex]; - final entryList = _logEntryDailyMap[date]; - final tiles = []; - for (LogEntry logEntry in entryList!) { - double bolus = - LogBolus.getTotalBolusForEntry(logEntry.id); - double carbs = - LogMeal.getTotalCarbsForEntry(logEntry.id); - TextStyle glucoseStyle = TextStyle( - color: GlucoseTarget.getColorForGlucose( - mgPerDl: logEntry.mgPerDl ?? 0, - mmolPerL: logEntry.mmolPerL ?? 0)); - tiles.add( - Card( - child: ListTile( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - LogEntryScreen(id: logEntry.id), - ), - ).then((result) => reload(message: result?[0])); - }, - title: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - DateTimeUtils.displayTime(logEntry.time), - ), - ), - Expanded( - child: Column( - children: logEntry.mgPerDl != null && - (Settings.glucoseMeasurement == - GlucoseMeasurement - .mgPerDl || - Settings.glucoseDisplayMode == - GlucoseDisplayMode.both || - Settings.glucoseDisplayMode == - GlucoseDisplayMode - .bothForList) - ? [ - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Text( - logEntry.mgPerDl.toString(), - style: glucoseStyle), - logEntry.glucoseTrend != null - ? Transform.rotate( - angle: logEntry - .glucoseTrend! * - math.pi / - 180, - child: Icon( - Icons.arrow_upward, - color: glucoseStyle - .color, - size: 16.0, - ), - ) - : Container(), - ], - ), - const Text( - 'mg/dl', - textScaleFactor: 0.75, - ), - ] - : [], - ), - ), - Expanded( - child: Column( - children: logEntry.mmolPerL != null && - (Settings.glucoseMeasurement == - GlucoseMeasurement - .mmolPerL || - Settings.glucoseDisplayMode == - GlucoseDisplayMode.both || - Settings.glucoseDisplayMode == - GlucoseDisplayMode - .bothForList) - ? [ - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Text( - logEntry.mmolPerL - .toString(), - style: glucoseStyle), - logEntry.glucoseTrend != null - ? Transform.rotate( - angle: logEntry - .glucoseTrend! * - math.pi / - 180, - child: Icon( - Icons.arrow_upward, - color: glucoseStyle - .color, - size: 16.0, - ), - ) - : Container(), - ], - ), - const Text( - 'mmol/l', - textScaleFactor: 0.75, - ), - ] - : [], - ), - ), - Expanded( - child: Column( - children: (bolus > 0) - ? [ - Text(bolus.toStringAsPrecision(3)), - const Text('U', - textScaleFactor: 0.75), - ] - : [], - ), - ), - Expanded( - child: Column( - children: (carbs > 0) - ? [ - Text(carbs.toStringAsPrecision(3)), - Text( - '${Settings.nutritionMeasurementSuffix} carbs', - textScaleFactor: 0.75), - ] - : [], - ), - ), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () => handleDeleteAction(logEntry), - icon: const Icon(Icons.delete, - color: Colors.blue), - ) - ], - ), - ), - )); - } - return ListBody( - children: [ - Padding( - padding: const EdgeInsets.all(10.0), + itemCount: _logEntries.length, + itemBuilder: (context, index) { + LogEntry logEntry = _logEntries[index]; + double bolus = + LogBolus.getTotalBolusForEntry(logEntry.id); + double carbs = + LogMeal.getTotalCarbsForEntry(logEntry.id); + TextStyle glucoseStyle = TextStyle( + color: GlucoseTarget.getColorForGlucose( + mgPerDl: logEntry.mgPerDl ?? 0, + mmolPerL: logEntry.mmolPerL ?? 0)); + return Card( + child: ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + LogEntryScreen(id: logEntry.id), + ), + ).then((result) => reload(message: result?[0])); + }, + title: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( child: Text( - DateTimeUtils.displayDate(date).toUpperCase(), - style: Theme.of(context).textTheme.subtitle2, + DateTimeUtils.displayTime(logEntry.time), ), + ), + Expanded( + child: Column( + children: logEntry.mgPerDl != null && + (Settings.glucoseMeasurement == + GlucoseMeasurement + .mgPerDl || + Settings.glucoseDisplayMode == + GlucoseDisplayMode.both || + Settings.glucoseDisplayMode == + GlucoseDisplayMode + .bothForList) + ? [ + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + logEntry.mgPerDl.toString(), + style: glucoseStyle), + logEntry.glucoseTrend != null + ? Transform.rotate( + angle: logEntry + .glucoseTrend! * + math.pi / + 180, + child: Icon( + Icons.arrow_upward, + color: glucoseStyle + .color, + size: 16.0, + ), + ) + : Container(), + ], + ), + const Text( + 'mg/dl', + textScaleFactor: 0.75, + ), + ] + : [], + ), + ), + Expanded( + child: Column( + children: logEntry.mmolPerL != null && + (Settings.glucoseMeasurement == + GlucoseMeasurement + .mmolPerL || + Settings.glucoseDisplayMode == + GlucoseDisplayMode.both || + Settings.glucoseDisplayMode == + GlucoseDisplayMode + .bothForList) + ? [ + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + logEntry.mmolPerL + .toString(), + style: glucoseStyle), + logEntry.glucoseTrend != null + ? Transform.rotate( + angle: logEntry + .glucoseTrend! * + math.pi / + 180, + child: Icon( + Icons.arrow_upward, + color: glucoseStyle + .color, + size: 16.0, + ), + ) + : Container(), + ], + ), + const Text( + 'mmol/l', + textScaleFactor: 0.75, + ), + ] + : [], + ), + ), + Expanded( + child: Column( + children: (bolus > 0) + ? [ + Text(bolus.toStringAsPrecision(3)), + const Text('U', + textScaleFactor: 0.75), + ] + : [], + ), + ), + Expanded( + child: Column( + children: (carbs > 0) + ? [ + Text(carbs.toStringAsPrecision(3)), + Text( + '${Settings.nutritionMeasurementSuffix} carbs', + textScaleFactor: 0.75), + ] + : [], + ), + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () => handleDeleteAction(logEntry), + icon: const Icon(Icons.delete, + color: Colors.blue), ) - ] + - tiles + - [const Divider()] + ], + ), + ), ); }, ), ) : const Center( - child: Text('You have not created any Log Entries yet!'), + child: Text( + 'You have not created any Log Entries for this date yet!'), ), ), ], ), floatingActionButton: FloatingActionButton( onPressed: () { + final now = DateTime.now(); Navigator.push( context, MaterialPageRoute( - builder: (context) => const LogEntryScreen(), + builder: (context) => LogEntryScreen( + suggestedDate: _date.isAtSameMomentAs(DateTime(now.year, now.month, now.day)) ? now : _date), ), ).then((result) => reload(message: result?[0])); }, diff --git a/lib/screens/log/log_entry/log_bolus_detail.dart b/lib/screens/log/log_entry/log_bolus_detail.dart index b9cc7b4..bb70fcd 100644 --- a/lib/screens/log/log_entry/log_bolus_detail.dart +++ b/lib/screens/log/log_entry/log_bolus_detail.dart @@ -135,7 +135,7 @@ class _LogBolusDetailScreenState extends State { _bolusType = BolusType.glucose; } - updateDelayedRatio(); + calculateBolus(); } @override @@ -200,76 +200,6 @@ class _LogBolusDetailScreenState extends State { } } - void updateDelayedRatio( - {double? totalUnitsUpdate, - double? delayedUnitsUpdate, - double? immediateUnitsUpdate, - double? percentageUpdate}) { - int precision = Utils.getFractionDigitsLength(Settings.insulinSteps); - double? totalUnits = - totalUnitsUpdate ?? double.tryParse(_unitsController.text); - double? delayedUnits; - double? immediateUnits; - - if (totalUnits == null) { - delayedUnits = - delayedUnitsUpdate ?? double.tryParse(_delayedUnitsController.text); - immediateUnits = immediateUnitsUpdate ?? - double.tryParse(_immediateUnitsController.text); - - if (percentageUpdate != null) { - if (delayedUnits != null) { - totalUnits = delayedUnits / percentageUpdate * 100; - } else if (immediateUnits != null) { - totalUnits = immediateUnits / percentageUpdate * 100; - } - } else if (delayedUnits != null && immediateUnits != null) { - totalUnits = Utils.addDoublesWithPrecision( - delayedUnits, immediateUnits, precision); - } - } - - setState(() { - _unitsController.text = (totalUnits ?? 0).toString(); - }); - - if (totalUnits != null) { - double percentage = percentageUpdate ?? _delayPercentage; - - if (totalUnitsUpdate != null || percentageUpdate != null) { - delayedUnits = totalUnits * percentage / 100; - } else if (delayedUnitsUpdate != null) { - delayedUnits = delayedUnitsUpdate; - } else if (immediateUnitsUpdate != null) { - delayedUnits = totalUnits - immediateUnitsUpdate; - } - - if (delayedUnits != null) { - double remainder = delayedUnits % Settings.insulinSteps; - int precision = Utils.getFractionDigitsLength(Settings.insulinSteps); - - if (remainder != 0) { - delayedUnits = Utils.addDoublesWithPrecision( - delayedUnits, -remainder, precision); - if (remainder > Settings.insulinSteps / 2) { - delayedUnits = Utils.addDoublesWithPrecision( - delayedUnits, Settings.insulinSteps, precision); - } - } - - setState(() { - _delayedUnitsController.text = delayedUnits.toString(); - _immediateUnitsController.text = Utils.addDoublesWithPrecision( - totalUnits!, -delayedUnits!, precision) - .toString(); - if (totalUnits != 0) { - _delayPercentage = delayedUnits * 100 / totalUnits; - } - }); - } - } - } - void onSelectMeal(LogMeal? meal) { updateLogMeal(meal); if (meal != null && meal.totalCarbs != null) { @@ -282,25 +212,26 @@ class _LogBolusDetailScreenState extends State { void calculateBolus() { if (_rate != null && !_setManually) { - double units = (double.tryParse(_carbsController.text) ?? 0) / - (_rate!.carbs / _rate!.units); - double remainder = units % Settings.insulinSteps; - int precision = Utils.getFractionDigitsLength(Settings.insulinSteps); + double? units; - if (remainder != 0) { - units = Utils.addDoublesWithPrecision(units, -remainder, precision); - if (remainder > Settings.insulinSteps / 2) { - units = Utils.addDoublesWithPrecision( - units, Settings.insulinSteps, precision); + if (_bolusType == BolusType.glucose) { + if (Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl) { + units = (int.tryParse(_mgPerDlCorrectionController.text) ?? 0) / + (_rate!.mgPerDl ?? 1) / + _rate!.units; + } + if (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL) { + units = (int.tryParse(_mmolPerLCorrectionController.text) ?? 0) / + (_rate!.mmolPerL ?? 1) / + _rate!.units; } } + if (_bolusType == BolusType.meal) { + units = (double.tryParse(_carbsController.text) ?? 0) / + (_rate!.carbs / _rate!.units); + } - setState(() { - _unitsController.text = units.toString(); - }); - - updateDelayedRatio( - totalUnitsUpdate: double.tryParse(_unitsController.text)); + updateDelayedRatio(totalUnitsUpdate: units); } } @@ -339,11 +270,7 @@ class _LogBolusDetailScreenState extends State { Utils.convertMgPerDlToMmolPerL(mgPerDlTarget ?? 0).toString(); _mmolPerLCorrectionController.text = Utils.convertMgPerDlToMmolPerL(mgPerDlCorrection ?? 0).toString(); - if (_rate != null && !_setManually) { - updateDelayedRatio( - totalUnitsUpdate: (mgPerDlCorrection ?? 0) / - ((_rate!.mgPerDl ?? 0) / _rate!.units)); - } + calculateBolus(); }); } if ((mmolPerLCurrent != null && mgPerDlCurrent == null) || @@ -357,15 +284,93 @@ class _LogBolusDetailScreenState extends State { Utils.convertMmolPerLToMgPerDl(mmolPerLTarget ?? 0).toString(); _mgPerDlCorrectionController.text = Utils.convertMmolPerLToMgPerDl(mmolPerLCorrection ?? 0).toString(); - if (_rate != null && !_setManually) { - updateDelayedRatio( - totalUnitsUpdate: (mmolPerLCorrection ?? 0) / - ((_rate!.mmolPerL ?? 0) / _rate!.units)); - } + calculateBolus(); }); } } + void updateDelayedRatio({ + double? totalUnitsUpdate, + double? delayedUnitsUpdate, + double? immediateUnitsUpdate, + double? percentageUpdate + }) { + int precision = Utils.getFractionDigitsLength(Settings.insulinSteps); + + double? totalUnits; + double? delayedUnits; + double? immediateUnits; + + if (totalUnitsUpdate != null) { + totalUnits = Utils.roundToMultipleOfBase(totalUnitsUpdate, Settings.insulinSteps); + } else if (double.tryParse(_unitsController.text) != null) { + totalUnits = Utils.roundToMultipleOfBase(double.tryParse(_unitsController.text)!, Settings.insulinSteps); + } + + if (delayedUnitsUpdate != null) { + delayedUnits = Utils.roundToMultipleOfBase(delayedUnitsUpdate, Settings.insulinSteps); + } else if (double.tryParse(_delayedUnitsController.text) != null) { + delayedUnits = Utils.roundToMultipleOfBase(double.tryParse(_delayedUnitsController.text)!, Settings.insulinSteps); + } + + if (immediateUnitsUpdate != null) { + immediateUnits = Utils.roundToMultipleOfBase(immediateUnitsUpdate, Settings.insulinSteps); + } else if (double.tryParse(_immediateUnitsController.text) != null) { + immediateUnits = Utils.roundToMultipleOfBase(double.tryParse(_immediateUnitsController.text)!, Settings.insulinSteps); + } + + if (totalUnits == null) { + if (percentageUpdate != null) { + if (immediateUnits != null) { + totalUnits = immediateUnits / (100 - percentageUpdate) * 100; + } else if (delayedUnits != null) { + totalUnits = delayedUnits / percentageUpdate * 100; + } + } else if (delayedUnits != null && immediateUnits != null) { + totalUnits = Utils.addDoublesWithPrecision( + delayedUnits, immediateUnits, precision); + } + + if (totalUnits != null) { + totalUnits = + Utils.roundToMultipleOfBase(totalUnits, Settings.insulinSteps); + } + } + + setState(() { + _unitsController.text = Utils.toStringMatchingTemplateFractionPrecision( + totalUnits ?? 0, Settings.insulinSteps); + }); + + if (totalUnits != null) { + double percentage = percentageUpdate ?? _delayPercentage; + + if (totalUnitsUpdate != null || percentageUpdate != null) { + immediateUnits = Utils.roundToMultipleOfBase( + totalUnits * (100 - percentage) / 100, Settings.insulinSteps); + } else if (delayedUnitsUpdate != null) { + immediateUnits = totalUnits - delayedUnits!; + } + + if (immediateUnits != null) { + delayedUnits = Utils.addDoublesWithPrecision( + totalUnits, -immediateUnits, precision); + + setState(() { + _immediateUnitsController.text = + Utils.toStringMatchingTemplateFractionPrecision( + immediateUnits!, Settings.insulinSteps); + _delayedUnitsController.text = + Utils.toStringMatchingTemplateFractionPrecision( + delayedUnits!, Settings.insulinSteps); + if (totalUnits != 0) { + _delayPercentage = delayedUnits / totalUnits! * 100; + } + }); + } + } + } + void handleSaveAction() async { setState(() { _isSaving = true; @@ -560,7 +565,7 @@ class _LogBolusDetailScreenState extends State { onChanged: (_) { setState(() { _bolusType = BolusType.glucose; - onChangeGlucose(); + calculateBolus(); }); }), ), @@ -741,7 +746,8 @@ class _LogBolusDetailScreenState extends State { controller: _carbsController, step: Settings.nutritionSteps, onChanged: (value) { - _carbsController.text = (value ?? 0).toString(); + _carbsController.text = + (value ?? 0).toString(); calculateBolus(); }, ), @@ -768,11 +774,9 @@ class _LogBolusDetailScreenState extends State { value: _delayPercentage, min: 0, max: 100, - onChanged: _delayController.text != '' - ? (value) { - updateDelayedRatio(percentageUpdate: value); - } - : null, + onChanged: (value) { + updateDelayedRatio(percentageUpdate: value); + }, ), ), const Text('%', textScaleFactor: 1.5), @@ -800,15 +804,16 @@ class _LogBolusDetailScreenState extends State { child: Padding( padding: const EdgeInsets.only(left: 5.0), child: NumberFormField( - label: 'Delayed Bolus', - suffix: ' U', - controller: _delayedUnitsController, - max: double.tryParse(_unitsController.text), - step: Settings.insulinSteps, - readOnly: true, - onChanged: (value) => updateDelayedRatio( - delayedUnitsUpdate: value), - ), + label: 'Delayed Bolus', + suffix: ' U', + controller: _delayedUnitsController, + max: double.tryParse(_unitsController.text), + step: Settings.insulinSteps, + readOnly: true, + onChanged: (value) => { + updateDelayedRatio( + delayedUnitsUpdate: value), + }), ), ), ] diff --git a/lib/screens/log/log_entry/log_entry.dart b/lib/screens/log/log_entry/log_entry.dart index 2cdd87b..5878ec6 100644 --- a/lib/screens/log/log_entry/log_entry.dart +++ b/lib/screens/log/log_entry/log_entry.dart @@ -21,8 +21,9 @@ import 'dart:math' as math; class LogEntryScreen extends StatefulWidget { static const String routeName = '/log-entry'; final int id; + final DateTime? suggestedDate; - const LogEntryScreen({Key? key, this.id = 0}) : super(key: key); + const LogEntryScreen({Key? key, this.id = 0, this.suggestedDate}) : super(key: key); @override _LogEntryScreenState createState() => _LogEntryScreenState(); @@ -43,10 +44,8 @@ class _LogEntryScreenState extends State { final _timeController = TextEditingController(text: ''); final _dateController = TextEditingController(text: ''); - final _mgPerDlController = - TextEditingController(text: Settings.targetMgPerDl.toString()); - final _mmolPerLController = - TextEditingController(text: Settings.targetMmolPerL.toString()); + final _mgPerDlController = TextEditingController(text: ''); + final _mmolPerLController = TextEditingController(text: ''); final _notesController = TextEditingController(text: ''); late FloatingActionButton addMealButton; @@ -108,7 +107,7 @@ class _LogEntryScreenState extends State { _glucoseTrend = _logEntry!.glucoseTrend; _notesController.text = _logEntry!.notes ?? ''; } else { - _time = DateTime.now(); + _time = widget.suggestedDate ?? DateTime.now(); } updateTime(); @@ -364,7 +363,7 @@ class _LogEntryScreenState extends State { ? 2 : 1, child: NumberFormField( - label: 'per mg/dl', + label: 'Blood Glucose', suffix: 'mg/dl', readOnly: Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL, @@ -389,7 +388,7 @@ class _LogEntryScreenState extends State { ? 2 : 1, child: NumberFormField( - label: 'per mmol/l', + label: 'Blood Glucose', suffix: 'mmol/l', readOnly: Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl, diff --git a/lib/screens/log/log_entry/log_meal_detail.dart b/lib/screens/log/log_entry/log_meal_detail.dart index a0a2884..6704d9a 100644 --- a/lib/screens/log/log_entry/log_meal_detail.dart +++ b/lib/screens/log/log_entry/log_meal_detail.dart @@ -1,4 +1,5 @@ import 'package:diameter/components/detail.dart'; +import 'package:diameter/components/forms/boolean_form_field.dart'; import 'package:diameter/components/forms/number_form_field.dart'; import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/components/forms/auto_complete_dropdown_button.dart'; @@ -37,10 +38,13 @@ class _LogMealDetailScreenState extends State { bool _isNew = true; bool _isSaving = false; bool _isExpanded = false; + bool _setManually = false; final GlobalKey _logMealForm = GlobalKey(); final ScrollController _scrollController = ScrollController(); + double _amount = 1; + final _valueController = TextEditingController(text: ''); final _carbsRatioController = TextEditingController(text: ''); final _portionSizeController = TextEditingController(text: ''); @@ -271,74 +275,111 @@ class _LogMealDetailScreenState extends State { } } - void updateAmount(double? amount) { - double previousAmount; - double newAmount; - double? portionSize; - double? carbsRatio; - - previousAmount = double.tryParse(_amountController.text) ?? 1; - newAmount = amount?.toDouble() ?? 1; - - setState(() { - _amountController.text = newAmount.toString(); - }); - - if (_carbsRatioController.text != '') { - carbsRatio = double.tryParse(_carbsRatioController.text); - } - if (_portionSizeController.text != '') { - portionSize = double.tryParse(_portionSizeController.text); - } - - if (portionSize != null) { + void updateAmount(double? newAmount) { + if (newAmount != null) { setState(() { - portionSize = portionSize! / previousAmount * newAmount; - _portionSizeController.text = portionSize.toString(); + _amountController.text = Utils.getFractionDigitsLength(newAmount) == 0 + ? newAmount.toInt().toString() + : newAmount.toString(); }); - if (carbsRatio != null) { - setState(() { - _totalCarbsController.text = - Utils.calculateCarbs(carbsRatio!, portionSize!).toString(); - }); + + double? portionSize; + double? basePortionSize; + + if (_portionSizeController.text != '') { + portionSize = double.tryParse(_portionSizeController.text); } + + if (portionSize != null && portionSize != 0) { + basePortionSize = portionSize / _amount; + } else if (_meal != null) { + basePortionSize = _meal!.portionSize; + } + + if (basePortionSize != null) { + setState(() { + portionSize = basePortionSize! * newAmount; + _portionSizeController.text = + Utils.toStringMatchingTemplateFractionPrecision( + portionSize!, Settings.nutritionSteps); + }); + calculateThirdMeasurementOfPortionCarbsRelation( + portionSizeUpdate: portionSize); + } + + setState(() { + _amount = newAmount; + }); } } - void calculateThirdMeasurementOfPortionCarbsRelation() { - double? amount = double.tryParse(_amountController.text) ?? 1; - double? carbsRatio; - double? portionSize; - double? carbsPerPortion; + void calculateThirdMeasurementOfPortionCarbsRelation( + {double? carbsRatioUpdate, + double? portionSizeUpdate, + double? totalCarbsUpdate}) { + if (!_setManually) { + double? carbsRatio = + carbsRatioUpdate ?? double.tryParse(_carbsRatioController.text); + double? portionSize = + portionSizeUpdate ?? double.tryParse(_portionSizeController.text); + double? totalCarbs = + totalCarbsUpdate ?? double.tryParse(_totalCarbsController.text); - if (_carbsRatioController.text != '') { - carbsRatio = double.tryParse(_carbsRatioController.text); - } - if (_portionSizeController.text != '') { - portionSize = double.tryParse(_portionSizeController.text); - } - if (_totalCarbsController.text != '') { - carbsPerPortion = double.tryParse(_totalCarbsController.text); - } + int toCalculate = 0; + const calcCarbsRatio = 1; + const calcTotalCarbs = 2; + const calcPortionSize = 3; + + if (carbsRatioUpdate != null) { + if (portionSize != null && portionSize != 0) { + toCalculate = calcTotalCarbs; + } else if (totalCarbs != null && totalCarbs != 0) { + toCalculate = calcPortionSize; + } + } else if (portionSizeUpdate != null) { + if (carbsRatio != null && carbsRatio != 0) { + toCalculate = calcTotalCarbs; + } else if (totalCarbs != null && totalCarbs != 0) { + toCalculate = calcCarbsRatio; + } + } else if (totalCarbsUpdate != null) { + if (carbsRatio != null && carbsRatio != 0) { + toCalculate = calcPortionSize; + } else if (portionSize != null && portionSize != 0) { + toCalculate = calcCarbsRatio; + } + } else { + if (carbsRatio != null && carbsRatio != 0) { + if (portionSize != null && portionSize != 0) { + toCalculate = calcTotalCarbs; + } else if (totalCarbs != null && totalCarbs != 0) { + toCalculate = calcPortionSize; + } + } else if (portionSize != null && + portionSize != 0 && + totalCarbs != null && + totalCarbs != 0) { + toCalculate = calcCarbsRatio; + } + } - if (carbsRatio != null && portionSize != null && carbsPerPortion == null) { setState(() { - _totalCarbsController.text = - Utils.calculateCarbs(carbsRatio!, portionSize! * amount).toString(); - }); - } - if (carbsRatio == null && portionSize != null && carbsPerPortion != null) { - setState(() { - _carbsRatioController.text = - Utils.calculateCarbsRatio(carbsPerPortion!, portionSize! * amount) - .toString(); - }); - } - if (carbsRatio != null && portionSize == null && carbsPerPortion != null) { - setState(() { - _portionSizeController.text = - Utils.calculatePortionSize(carbsRatio!, carbsPerPortion!) - .toString(); + if (toCalculate == calcCarbsRatio) { + _carbsRatioController.text = + Utils.calculateCarbsRatio(totalCarbs!, portionSize!).toString(); + } else if (toCalculate == calcTotalCarbs) { + _totalCarbsController.text = + Utils.toStringMatchingTemplateFractionPrecision( + Utils.calculateCarbs(carbsRatio!, portionSize!, + step: Settings.nutritionSteps), + Settings.nutritionSteps); + } else if (toCalculate == calcPortionSize) { + _portionSizeController.text = + Utils.toStringMatchingTemplateFractionPrecision( + Utils.calculatePortionSize(carbsRatio!, totalCarbs!, + step: Settings.nutritionSteps), + Settings.nutritionSteps); + } }); } } @@ -360,18 +401,6 @@ class _LogMealDetailScreenState extends State { FormWrapper( formState: _logMealForm, fields: [ - TextFormField( - controller: _valueController, - decoration: const InputDecoration( - labelText: 'Name', - ), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty name'; - } - return null; - }, - ), Row( children: [ Expanded( @@ -401,9 +430,22 @@ class _LogMealDetailScreenState extends State { ), ], ), + TextFormField( + controller: _valueController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty name'; + } + return null; + }, + ), Row( children: [ Expanded( + flex: 10, child: NumberFormField( controller: _amountController, label: 'Amount', @@ -411,31 +453,52 @@ class _LogMealDetailScreenState extends State { onChanged: updateAmount, ), ), - TextButton( - onPressed: () => updateAmount(0.5), - child: Column( - children: const [ - Text('1', style: TextStyle(decoration: TextDecoration.underline, decorationThickness: 2),), - Text('2'), - ], + Expanded( + child: TextButton( + onPressed: () => updateAmount(0.5), + child: Column( + children: const [ + Text( + '1', + style: TextStyle( + decoration: TextDecoration.underline, + decorationThickness: 2), + ), + Text('2'), + ], + ), ), ), - TextButton( - onPressed: () => updateAmount(0.33), - child: Column( - children: const [ - Text('1', style: TextStyle(decoration: TextDecoration.underline, decorationThickness: 2),), - Text('3'), - ], + Expanded( + child: TextButton( + onPressed: () => updateAmount(0.33), + child: Column( + children: const [ + Text( + '1', + style: TextStyle( + decoration: TextDecoration.underline, + decorationThickness: 2), + ), + Text('3'), + ], + ), ), ), - TextButton( - onPressed: () => updateAmount(0.67), - child: Column( - children: const [ - Text('2', style: TextStyle(decoration: TextDecoration.underline, decorationThickness: 2),), - Text('3'), - ], + Expanded( + child: TextButton( + onPressed: () => updateAmount(0.67), + child: Column( + children: const [ + Text( + '2', + style: TextStyle( + decoration: TextDecoration.underline, + decorationThickness: 2), + ), + Text('3'), + ], + ), ), ), ], @@ -443,54 +506,64 @@ class _LogMealDetailScreenState extends State { Row( children: [ Expanded( - child: TextFormField( - decoration: InputDecoration( - labelText: 'Portion size', - suffixText: Settings.nutritionMeasurementSuffix, - ), + child: NumberFormField( + label: 'Portion size', + suffix: Settings.nutritionMeasurementSuffix, controller: _portionSizeController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) async { + showSteppers: false, + autoRoundToMultipleOfStep: true, + step: Settings.nutritionSteps, + onChanged: (value) async { await Future.delayed(const Duration(seconds: 1)); - calculateThirdMeasurementOfPortionCarbsRelation(); + calculateThirdMeasurementOfPortionCarbsRelation( + portionSizeUpdate: value); }, ), ), const SizedBox(width: 10), Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Carbs ratio', - suffixText: '%', - ), + child: NumberFormField( + label: 'Carbs ratio', + suffix: '%', controller: _carbsRatioController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) async { + showSteppers: false, + onChanged: (value) async { await Future.delayed(const Duration(seconds: 1)); - calculateThirdMeasurementOfPortionCarbsRelation(); + calculateThirdMeasurementOfPortionCarbsRelation( + carbsRatioUpdate: value); }, ), ), const SizedBox(width: 10), Expanded( - child: TextFormField( - decoration: InputDecoration( - labelText: 'Total carbs', - suffixText: Settings.nutritionMeasurementSuffix, - ), + child: NumberFormField( + label: 'Total carbs', + suffix: Settings.nutritionMeasurementSuffix, controller: _totalCarbsController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) async { + showSteppers: false, + autoRoundToMultipleOfStep: true, + step: Settings.nutritionSteps, + onChanged: (value) async { await Future.delayed(const Duration(seconds: 1)); - calculateThirdMeasurementOfPortionCarbsRelation(); + calculateThirdMeasurementOfPortionCarbsRelation( + totalCarbsUpdate: value); }, ), ), ], ), + Expanded( + child: BooleanFormField( + value: _setManually, + label: 'set carbs ratio manually', + onChanged: (value) { + setState(() { + _setManually = value; + calculateThirdMeasurementOfPortionCarbsRelation(); + }); + }, + ), + ), TextFormField( controller: _notesController, decoration: const InputDecoration( diff --git a/lib/screens/log/log_event/log_event_detail.dart b/lib/screens/log/log_event/log_event_detail.dart index 414db67..93ec4ab 100644 --- a/lib/screens/log/log_event/log_event_detail.dart +++ b/lib/screens/log/log_event/log_event_detail.dart @@ -22,9 +22,10 @@ class LogEventDetailScreen extends StatefulWidget { final int logEntryId; final int endLogEntryId; final int id; + final DateTime? suggestedDate; const LogEventDetailScreen( - {Key? key, this.logEntryId = 0, this.endLogEntryId = 0, this.id = 0}) + {Key? key, this.logEntryId = 0, this.endLogEntryId = 0, this.id = 0, this.suggestedDate}) : super(key: key); @override @@ -93,7 +94,7 @@ class _LogEventDetailScreenState extends State { _time = _logEvent!.time; _endTime = _logEvent!.endTime; } else { - _time = DateTime.now(); + _time = widget.suggestedDate ?? DateTime.now(); } _logEventTypes = LogEventType.getAll(); diff --git a/lib/screens/log/log_event/log_event_list.dart b/lib/screens/log/log_event/log_event_list.dart index 62fee4d..6ab1dc9 100644 --- a/lib/screens/log/log_event/log_event_list.dart +++ b/lib/screens/log/log_event/log_event_list.dart @@ -16,25 +16,36 @@ class LogEventListScreen extends StatefulWidget { class _LogEventListScreenState extends State { List _activeEvents = []; - late Map> _logEventDailyMap; + late List _logEvents; + final ScrollController _scrollController = ScrollController(); + final TextEditingController _dateController = TextEditingController(text: ''); + + late DateTime _date; + bool _showActive = true; + @override void initState() { super.initState(); + + _date = DateTime.now(); + _dateController.text = DateTimeUtils.displayDate(_date); + reload(); } @override void dispose() { _scrollController.dispose(); + _dateController.dispose(); super.dispose(); } void reload({String? message}) { setState(() { _activeEvents = LogEvent.getAllActiveForTime(DateTime.now()); - _logEventDailyMap = LogEvent.getDailyEntryMap(); + _logEvents = LogEvent.getAllForDate(_date); }); setState(() { @@ -51,11 +62,14 @@ class _LogEventListScreenState extends State { } void handleAddNewEvent() async { + final now = DateTime.now(); Navigator.push( context, MaterialPageRoute( builder: (context) { - return const LogEventDetailScreen(); + return LogEventDetailScreen( + suggestedDate: _date.isAtSameMomentAs(DateTime(now.year, now.month, now.day)) ? now : _date, + ); }, ), ).then((result) => reload(message: result?[0])); @@ -108,12 +122,25 @@ class _LogEventListScreenState extends State { } } + void onChangeDate(DateTime? date) { + if (date != null) { + setState(() { + _date = DateTime(date.year, date.month, date.day); + _dateController.text = DateTimeUtils.displayDate(date); + }); + reload(); + } + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Log Events'), actions: [ + IconButton( + onPressed: () => onChangeDate(DateTime.now()), + icon: const Icon(Icons.today)), IconButton(onPressed: reload, icon: const Icon(Icons.refresh)) ], ), @@ -121,108 +148,211 @@ class _LogEventListScreenState extends State { body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: _logEventDailyMap.isNotEmpty - ? Scrollbar( - controller: _scrollController, - child: ListView.builder( - controller: _scrollController, - shrinkWrap: true, - padding: const EdgeInsets.all(10.0), - itemCount: _logEventDailyMap.length, - itemBuilder: (context, dateIndex) { - List dateList = (_activeEvents.isNotEmpty - ? [null] - : []) + - _logEventDailyMap.keys.toList(); - final date = dateList[dateIndex]; - final eventList = date != null - ? _logEventDailyMap[date] - : _activeEvents; - final tiles = []; - if (eventList != null) { - for (LogEvent event in eventList) { - tiles.add(Card( - child: ListTile( - onTap: () { - handleEditAction(event); - }, - title: Row( - crossAxisAlignment: - CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text(date == null - ? DateTimeUtils - .displayDateTime( - event.time) - : DateTimeUtils.displayTime( - event.isEndEvent - ? event.endTime - : event.time), - ), - const SizedBox(width: 24), - Expanded( - child: Text( - (event.title ?? event.eventType.target?.value ?? '').toUpperCase(), - style: Theme.of(context).textTheme.subtitle2, - ), - ), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - event.hasEndTime && - event.endTime == null - ? IconButton( - icon: const Icon( - Icons.stop, - color: Colors.blue, - ), - onPressed: () => - handleStopAction(event), - ) - : const SizedBox(width: 50), - IconButton( - icon: const Icon( - Icons.edit, - color: Colors.blue, - ), - onPressed: () => - handleEditAction(event), - ), - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.blue, - ), - onPressed: () => - handleDeleteAction(event), - ), - ], - ), - ), - )); - } - } - return eventList != null && eventList.isNotEmpty ? ListBody( - children: [ - Padding( - padding: const EdgeInsets.all(10.0), + GestureDetector( + onTap: () => setState(() { + _showActive = !_showActive; + }), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Text( + 'ACTIVE EVENTS', + style: Theme.of(context).textTheme.subtitle2, + textAlign: TextAlign.center, + ), + ), + Icon(_showActive + ? Icons.expand_less + : Icons.expand_more), + ], + ), + ), + ), + !_showActive ? Container() : + _activeEvents.isNotEmpty + ? ListView.builder( + shrinkWrap: true, + padding: const EdgeInsets.all(10.0), + itemCount: _activeEvents.length, + itemBuilder: (context, index) { + LogEvent event = _activeEvents[index]; + return Card( + child: ListTile( + onTap: () { + handleEditAction(event); + }, + title: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + DateTimeUtils.displayDateTime(event.time), + ), + const SizedBox(width: 24), + Expanded( child: Text( - DateTimeUtils.displayDate(date, fallback: 'Active Events').toUpperCase(), + (event.title ?? + event.eventType.target?.value ?? + '') + .toUpperCase(), style: Theme.of(context).textTheme.subtitle2, ), ), - ] + tiles + - [const Divider()], - ) : Container(); - }, - ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + event.hasEndTime && event.endTime == null + ? IconButton( + icon: const Icon( + Icons.stop, + color: Colors.blue, + ), + onPressed: () => handleStopAction(event), + ) + : const SizedBox(width: 50), + IconButton( + icon: const Icon( + Icons.edit, + color: Colors.blue, + ), + onPressed: () => handleEditAction(event), + ), + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => handleDeleteAction(event), + ), + ], + ), + ), + ); + }, ) + : const Center( + child: Text('There are no Active Events!'), + ), + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider(), + ), + Row( + children: [ + IconButton( + onPressed: _date.isAtSameMomentAs(DateTime(2000, 1, 1)) + ? null + : () => + onChangeDate(_date.subtract(const Duration(days: 1))), + icon: const Icon(Icons.arrow_back), + ), + Expanded( + child: GestureDetector( + onTap: () async { + final newTime = await showDatePicker( + context: context, + initialDate: _date, + firstDate: DateTime(2000, 1, 1), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + onChangeDate(newTime); + }, + child: Expanded( + child: Text( + DateTimeUtils.displayDate(_date).toUpperCase(), + style: Theme.of(context).textTheme.subtitle2, + textAlign: TextAlign.center, + ), + ), + ), + ), + IconButton( + onPressed: + _date.add(const Duration(days: 1)).isBefore(DateTime.now()) + ? () => onChangeDate(_date.add(const Duration(days: 1))) + : null, + icon: const Icon(Icons.arrow_forward), + ), + ], + ), + Expanded( + child: _logEvents.isNotEmpty + ? Scrollbar( + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + shrinkWrap: true, + padding: const EdgeInsets.all(10.0), + itemCount: _logEvents.length, + itemBuilder: (context, index) { + LogEvent event = _logEvents[index]; + return Card( + child: ListTile( + onTap: () { + handleEditAction(event); + }, + title: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + DateTimeUtils.displayTime(event.isEndEvent + ? event.endTime + : event.time), + ), + const SizedBox(width: 24), + Expanded( + child: Text( + (event.title ?? + event.eventType.target?.value ?? + '') + .toUpperCase(), + style: + Theme.of(context).textTheme.subtitle2, + ), + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + event.hasEndTime && event.endTime == null + ? IconButton( + icon: const Icon( + Icons.stop, + color: Colors.blue, + ), + onPressed: () => + handleStopAction(event), + ) + : const SizedBox(width: 50), + IconButton( + icon: const Icon( + Icons.edit, + color: Colors.blue, + ), + onPressed: () => handleEditAction(event), + ), + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => handleDeleteAction(event), + ), + ], + ), + ), + ); + }, + )) : const Center( - child: Text('There are no Events!'), + child: Text('There are no Events for that date!'), ), ), ], diff --git a/lib/screens/meal/meal_detail.dart b/lib/screens/meal/meal_detail.dart index a1ef3b8..83a5588 100644 --- a/lib/screens/meal/meal_detail.dart +++ b/lib/screens/meal/meal_detail.dart @@ -1,4 +1,6 @@ import 'package:diameter/components/detail.dart'; +import 'package:diameter/components/forms/boolean_form_field.dart'; +import 'package:diameter/components/forms/number_form_field.dart'; import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/components/forms/auto_complete_dropdown_button.dart'; import 'package:diameter/components/forms/form_wrapper.dart'; @@ -32,6 +34,7 @@ class _MealDetailScreenState extends State { bool _isNew = true; bool _isSaving = false; bool _isExpanded = false; + bool _setManually = false; final GlobalKey _mealForm = GlobalKey(); final ScrollController _scrollController = ScrollController(); @@ -242,7 +245,7 @@ class _MealDetailScreenState extends State { } } - Future onSelectMealSource(MealSource? mealSource) async { + void onSelectMealSource(MealSource? mealSource) { setState(() { _mealSource = mealSource; _mealSourceController.text = (_mealSource ?? '').toString(); @@ -263,40 +266,72 @@ class _MealDetailScreenState extends State { } } - void calculateThirdMeasurementOfPortionCarbsRelation() { - double? carbsRatio; - double? portionSize; - double? carbsPerPortion; + void calculateThirdMeasurementOfPortionCarbsRelation( + {double? carbsRatioUpdate, + double? portionSizeUpdate, + double? carbsPerPortionUpdate}) { + if (!_setManually) { + double? carbsRatio = + carbsRatioUpdate ?? double.tryParse(_carbsRatioController.text); + double? portionSize = + portionSizeUpdate ?? double.tryParse(_portionSizeController.text); + double? carbsPerPortion = carbsPerPortionUpdate ?? + double.tryParse(_carbsPerPortionController.text); - if (_carbsRatioController.text != '') { - carbsRatio = double.tryParse(_carbsRatioController.text); - } - if (_portionSizeController.text != '') { - portionSize = double.tryParse(_portionSizeController.text); - } - if (_carbsRatioController.text != '') { - carbsPerPortion = double.tryParse(_carbsPerPortionController.text); + int toCalculate = 0; + const calcCarbsRatio = 1; + const calcCarbsPerPortion = 2; + const calcPortionSize = 3; + + if (carbsRatioUpdate != null) { + if (portionSize != null && portionSize != 0) { + toCalculate = calcCarbsPerPortion; + } else if (carbsPerPortion != null && carbsPerPortion != 0) { + toCalculate = calcPortionSize; + } + } else if (portionSizeUpdate != null) { + if (carbsRatio != null && carbsRatio != 0) { + toCalculate = calcCarbsPerPortion; + } else if (carbsPerPortion != null && carbsPerPortion != 0) { + toCalculate = calcCarbsRatio; + } + } else if (carbsPerPortionUpdate != null) { + if (carbsRatio != null && carbsRatio != 0) { + toCalculate = calcPortionSize; + } else if (portionSize != null && portionSize != 0) { + toCalculate = calcCarbsRatio; + } + } else { + if (carbsRatio != null && carbsRatio != 0) { + if (portionSize != null && portionSize != 0) { + toCalculate = calcCarbsPerPortion; + } else if (carbsPerPortion != null && carbsPerPortion != 0) { + toCalculate = calcPortionSize; + } + } else if (portionSize != null && + portionSize != 0 && + carbsPerPortion != null && + carbsPerPortion != 0) { + toCalculate = calcCarbsRatio; + } } - if (carbsRatio != null && portionSize != null && carbsPerPortion == null) { setState(() { - _carbsPerPortionController.text = - Utils.calculateCarbs(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(); + if (toCalculate == calcCarbsRatio) { + _carbsRatioController.text = + Utils.calculateCarbsRatio(carbsPerPortion!, portionSize!) + .toString(); + } else if (toCalculate == calcCarbsPerPortion) { + _carbsPerPortionController.text = Utils.calculateCarbs( + carbsRatio!, portionSize!, + step: Settings.nutritionSteps) + .toString(); + } else if (toCalculate == calcPortionSize) { + _portionSizeController.text = Utils.calculatePortionSize( + carbsRatio!, carbsPerPortion!, + step: Settings.nutritionSteps) + .toString(); + } }); } } @@ -393,54 +428,57 @@ class _MealDetailScreenState extends State { Row( children: [ Expanded( - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Carbs ratio', - suffixText: '%', - ), + child: NumberFormField( + label: 'Carbs ratio', + suffix: '%', controller: _carbsRatioController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) async { + showSteppers: false, + onChanged: (value) async { await Future.delayed(const Duration(seconds: 1)); - calculateThirdMeasurementOfPortionCarbsRelation(); + calculateThirdMeasurementOfPortionCarbsRelation(carbsRatioUpdate: value); }, ), ), const SizedBox(width: 10), Expanded( - child: TextFormField( - decoration: InputDecoration( - labelText: 'Portion size', - suffixText: Settings.nutritionMeasurementSuffix, - ), + child: NumberFormField( + label: 'Portion size', + suffix: Settings.nutritionMeasurementSuffix, controller: _portionSizeController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) async { + showSteppers: false, + onChanged: (value) async { await Future.delayed(const Duration(seconds: 1)); - calculateThirdMeasurementOfPortionCarbsRelation(); + calculateThirdMeasurementOfPortionCarbsRelation(portionSizeUpdate: value); }, ), ), const SizedBox(width: 10), Expanded( - child: TextFormField( - decoration: InputDecoration( - labelText: 'Carbs per portion', - suffixText: Settings.nutritionMeasurementSuffix, - ), + child: NumberFormField( + label: 'Carbs per portion', + suffix: Settings.nutritionMeasurementSuffix, controller: _carbsPerPortionController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) async { + showSteppers: false, + onChanged: (value) async { await Future.delayed(const Duration(seconds: 1)); - calculateThirdMeasurementOfPortionCarbsRelation(); + calculateThirdMeasurementOfPortionCarbsRelation(carbsPerPortionUpdate: value); }, ), ), ], ), + Expanded( + child: BooleanFormField( + value: _setManually, + label: 'set carbs ratio manually', + onChanged: (value) { + setState(() { + _setManually = value; + calculateThirdMeasurementOfPortionCarbsRelation(); + }); + }, + ), + ), TextFormField( controller: _notesController, decoration: const InputDecoration( @@ -463,8 +501,6 @@ class _MealDetailScreenState extends State { ], ), ), - // ignore: todo - // TODO: display according to time format Row( children: [ Expanded( @@ -486,13 +522,14 @@ class _MealDetailScreenState extends State { value: _delayedBolusPercentage, min: 0, max: 100, - onChanged: _delayedBolusDurationController.text != '' - ? (value) { - setState(() { - _delayedBolusPercentage = value; - }); - } : null - ), + onChanged: + _delayedBolusDurationController.text != '' + ? (value) { + setState(() { + _delayedBolusPercentage = value; + }); + } + : null), ), const Text('%', textScaleFactor: 1.5), ], diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 6ff0e66..be9e234 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,9 +1,24 @@ import 'dart:math'; class Utils { - static double roundToDecimalPlaces(double value, int precision) { - double mod = pow(10.0, precision).toDouble(); - return ((value * mod).round().toDouble() / mod); + // static double roundToDecimalPlaces(double value, int precision) { + // double mod = pow(10.0, precision).toDouble(); + // return ((value * mod).round().toDouble() / mod); + // } + + static double roundToMultipleOfBase(double value, double base) { + double result = value; + double remainder = value % base; + int precision = Utils.getFractionDigitsLength(base); + + if (remainder != 0) { + result = Utils.addDoublesWithPrecision(result, -remainder, precision); + if (remainder > base / 2) { + result = Utils.addDoublesWithPrecision(result, base, precision); + } + } + + return result; } static double addDoublesWithPrecision(double a, double b, int precision) { @@ -14,7 +29,7 @@ class Utils { static int getFractionDigitsLength(double value) { final fractionDigits = value.toString().split('.'); - return fractionDigits[1].length; + return fractionDigits[1] == '0' ? 0 : fractionDigits[1].length; } static String toStringMatchingTemplateFractionPrecision( @@ -23,29 +38,30 @@ class Utils { return value.toStringAsFixed(precision); } - static double convertMgPerDlToMmolPerL(int mgPerDl) { - return Utils.roundToDecimalPlaces(mgPerDl * 0.0555, 2); + static double convertMgPerDlToMmolPerL(int mgPerDl, {double step = 0.01}) { + return Utils.roundToMultipleOfBase(mgPerDl * 0.0555, step); } static int convertMmolPerLToMgPerDl(double mmolPerL) { return (mmolPerL * 18.018).round(); } - static double calculateCarbs(double carbsRatio, double portionSize) { - return Utils.roundToDecimalPlaces(carbsRatio * portionSize / 100, 2); + static double calculateCarbs(double carbsRatio, double portionSize, + {double step = 0.01}) { + return Utils.roundToMultipleOfBase(carbsRatio * portionSize / 100, step); } static double calculateCarbsRatio( - double carbsPerPortion, double portionSize) { + double carbsPerPortion, double portionSize, {double step = 0.01}) { return portionSize > 0 - ? Utils.roundToDecimalPlaces(carbsPerPortion * 100 / portionSize, 2) + ? Utils.roundToMultipleOfBase(carbsPerPortion * 100 / portionSize, step) : 0; } static double calculatePortionSize( - double carbsRatio, double carbsPerPortion) { + double carbsRatio, double carbsPerPortion, {double step = 0.01}) { return carbsRatio > 0 - ? Utils.roundToDecimalPlaces(carbsPerPortion * 100 / carbsRatio, 2) + ? Utils.roundToMultipleOfBase(carbsPerPortion * 100 / carbsRatio, step) : 0; } } diff --git a/objectbox/data.mdb b/objectbox/data.mdb index 1d9c8cc..c30d7a2 100644 Binary files a/objectbox/data.mdb and b/objectbox/data.mdb differ diff --git a/objectbox/lock.mdb b/objectbox/lock.mdb index 94222e1..d2ddfb8 100644 Binary files a/objectbox/lock.mdb and b/objectbox/lock.mdb differ