import 'dart:math'; 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'; import 'package:diameter/models/bolus.dart'; import 'package:diameter/models/log_bolus.dart'; import 'package:diameter/models/log_entry.dart'; import 'package:diameter/models/log_meal.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/screens/log/log_entry/log_meal_detail.dart'; import 'package:diameter/utils/utils.dart'; import 'package:flutter/material.dart'; enum BolusType { meal, glucose, } enum GlucoseParameter { mgdlCurrent, mgdlTarget, mgdlCorrection, mmolCurrent, mmolTarget, mmolCorrection, } class LogBolusDetailScreen extends StatefulWidget { static const String routeName = '/log-bolus'; final int logEntryId; final int id; const LogBolusDetailScreen({Key? key, this.logEntryId = 0, this.id = 0}) : super(key: key); @override _LogBolusDetailScreenState createState() => _LogBolusDetailScreenState(); } class _LogBolusDetailScreenState extends State { LogEntry? _logEntry; LogBolus? _logBolus; bool _isNew = true; bool _isSaving = false; final GlobalKey _logBolusForm = GlobalKey(); final ScrollController _scrollController = ScrollController(); final _unitsController = TextEditingController(text: ''); final _carbsController = TextEditingController(text: ''); final _mgPerDlCurrentController = TextEditingController(text: ''); final _mgPerDlTargetController = TextEditingController(text: ''); final _mgPerDlCorrectionController = TextEditingController(text: ''); final _mmolPerLCurrentController = TextEditingController(text: ''); final _mmolPerLTargetController = TextEditingController(text: ''); final _mmolPerLCorrectionController = TextEditingController(text: ''); final _delayController = TextEditingController(text: ''); final _notesController = TextEditingController(text: ''); final _delayedUnitsController = TextEditingController(text: ''); final _immediateUnitsController = TextEditingController(text: ''); final _mealController = TextEditingController(text: ''); bool _setManually = false; BolusType _bolusType = BolusType.meal; LogMeal? _meal; Bolus? _rate; double _delayPercentage = 0; List _logMeals = []; @override void initState() { super.initState(); reload(); _logEntry = LogEntry.get(widget.logEntryId); _logMeals = LogMeal.getRecentWithoutBolus(widget.logEntryId); if (widget.id != 0) { _carbsController.text = (_logBolus!.carbs ?? '').toString(); _delayController.text = (_logBolus!.delay ?? '').toString(); _notesController.text = _logBolus!.notes ?? ''; _setManually = _logBolus!.setManually; _meal = _logBolus!.meal.target; _mealController.text = (_meal ?? '').toString(); _rate = _logBolus!.rate.target; } _rate ??= Bolus.getRateForTime(_logEntry?.time); _mgPerDlCurrentController.text = (_logBolus?.mgPerDlCurrent ?? (LogEntry.hasUncorrectedGlucose(widget.logEntryId) ? _logEntry?.mgPerDl ?? 0 : 0)) .toString(); _mgPerDlTargetController.text = (_logBolus?.mgPerDlTarget ?? Settings.targetMgPerDl).toString(); _mgPerDlCorrectionController.text = (_logBolus?.mgPerDlCorrection ?? max( (int.tryParse(_mgPerDlCurrentController.text) ?? 0) - (int.tryParse(_mgPerDlTargetController.text) ?? 0), 0)) .toString(); _mmolPerLCurrentController.text = (_logBolus?.mmolPerLCurrent ?? (LogEntry.hasUncorrectedGlucose(widget.logEntryId) ? _logEntry?.mmolPerL ?? 0 : 0)) .toString(); _mmolPerLTargetController.text = (_logBolus?.mmolPerLTarget ?? Settings.targetMmolPerL).toString(); _mmolPerLCorrectionController.text = (_logBolus?.mmolPerLCorrection ?? max( (double.tryParse(_mmolPerLCurrentController.text) ?? 0) - (double.tryParse(_mmolPerLTargetController.text) ?? 0), 0)) .toString(); _unitsController.text = (_logBolus?.units ?? (_rate != null && !_setManually ? ((int.tryParse(_mgPerDlCorrectionController.text) ?? 0) / ((_rate!.mgPerDl ?? 0) / _rate!.units)) : 0)) .toString(); if (widget.id == 0 && LogEntry.hasUncorrectedGlucose(widget.logEntryId)) { _bolusType = BolusType.glucose; } updateDelayedRatio(); } @override void dispose() { _scrollController.dispose(); _unitsController.dispose(); _carbsController.dispose(); _mgPerDlCurrentController.dispose(); _mgPerDlTargetController.dispose(); _mgPerDlCorrectionController.dispose(); _mmolPerLCurrentController.dispose(); _mmolPerLTargetController.dispose(); _mmolPerLCorrectionController.dispose(); _delayController.dispose(); _notesController.dispose(); _delayedUnitsController.dispose(); _immediateUnitsController.dispose(); _mealController.dispose(); super.dispose(); } void reload({String? message}) { if (widget.id != 0) { setState(() { _logBolus = LogBolus.get(widget.id); }); } _isNew = _logBolus == null; setState(() { if (message != null) { var snackBar = SnackBar( content: Text(message), duration: const Duration(seconds: 2), ); ScaffoldMessenger.of(context) ..removeCurrentSnackBar() ..showSnackBar(snackBar); } }); } void updateLogMeal(LogMeal? value) { setState(() { _meal = value; _mealController.text = (_meal ?? '').toString(); }); if (_meal != null) { if (_meal!.totalCarbs != null) { _carbsController.text = (_meal!.totalCarbs).toString(); } if (_meal!.meal.hasValue) { if (_meal!.meal.target!.delayedBolusDuration != null) { _delayController.text = (_meal!.meal.target?.delayedBolusDuration).toString(); } if (_meal!.meal.target!.delayedBolusDuration != null) { _delayPercentage = _meal!.meal.target!.delayedBolusPercentage!; } } calculateBolus(); } } 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) { setState(() { _carbsController.text = meal.totalCarbs.toString(); calculateBolus(); }); } } 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); if (remainder != 0) { units = Utils.addDoublesWithPrecision(units, -remainder, precision); if (remainder > Settings.insulinSteps / 2) { units = Utils.addDoublesWithPrecision( units, Settings.insulinSteps, precision); } } setState(() { _unitsController.text = units.toString(); }); updateDelayedRatio( totalUnitsUpdate: double.tryParse(_unitsController.text)); } } void onChangeGlucose() { int? mgPerDlCurrent; int? mgPerDlTarget; int? mgPerDlCorrection; double? mmolPerLCurrent; double? mmolPerLTarget; double? mmolPerLCorrection; if (Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl && _mgPerDlCurrentController.text != '' && _mgPerDlTargetController.text != '') { mgPerDlCurrent = int.tryParse(_mgPerDlCurrentController.text); mgPerDlTarget = int.tryParse(_mgPerDlTargetController.text); mgPerDlCorrection = max((mgPerDlCurrent ?? 0) - (mgPerDlTarget ?? 0), 0); } if (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL && _mmolPerLCurrentController.text != '') { mmolPerLCurrent = double.tryParse(_mmolPerLCurrentController.text); mmolPerLTarget = double.tryParse(_mmolPerLTargetController.text); mmolPerLCorrection = max((mmolPerLCurrent ?? 0) - (mmolPerLTarget ?? 0), 0); } if ((mgPerDlCurrent != null && mmolPerLCurrent == null) || (mgPerDlTarget != null && mmolPerLTarget == null) || (mgPerDlCorrection != null && mmolPerLCorrection == null)) { setState(() { _mgPerDlCorrectionController.text = (mgPerDlCorrection ?? 0).toString(); _mmolPerLCurrentController.text = Utils.convertMgPerDlToMmolPerL(mgPerDlCurrent ?? 0).toString(); _mmolPerLTargetController.text = 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)); } }); } if ((mmolPerLCurrent != null && mgPerDlCurrent == null) || (mmolPerLTarget != null && mgPerDlTarget == null) || (mmolPerLCorrection != null && mgPerDlCorrection == null)) { setState(() { _mmolPerLCurrentController.text = (mmolPerLCorrection ?? 0).toString(); _mgPerDlCurrentController.text = Utils.convertMmolPerLToMgPerDl(mmolPerLCurrent ?? 0).toString(); _mgPerDlTargetController.text = 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)); } }); } } void handleSaveAction() async { setState(() { _isSaving = true; }); if (_logBolusForm.currentState!.validate()) { LogBolus logBolus; LogBolus? delayedBolus; if ((int.tryParse(_delayController.text) ?? 0) != 0 && _delayPercentage != 0 && _delayPercentage != 100) { logBolus = LogBolus( id: widget.id, units: double.tryParse(_immediateUnitsController.text) ?? 0, setManually: _setManually, notes: _notesController.text, ); delayedBolus = LogBolus( delay: int.tryParse(_delayController.text), units: double.tryParse(_delayedUnitsController.text) ?? 0, setManually: _setManually, notes: _notesController.text, ); } else { logBolus = LogBolus( id: widget.id, units: double.tryParse(_unitsController.text) ?? 0, delay: _delayPercentage == 100 ? int.tryParse(_delayController.text) : null, setManually: _setManually, notes: _notesController.text, ); } if (_bolusType == BolusType.meal) { logBolus.carbs = double.tryParse(_carbsController.text); if (delayedBolus != null) { delayedBolus.carbs = double.tryParse(_carbsController.text); } logBolus.mgPerDlCurrent = null; logBolus.mmolPerLCurrent = null; } else { logBolus.carbs = null; logBolus.mgPerDlCurrent = int.tryParse(_mgPerDlCurrentController.text); logBolus.mmolPerLCurrent = double.tryParse(_mmolPerLCurrentController.text); logBolus.mgPerDlTarget = int.tryParse(_mgPerDlTargetController.text); logBolus.mmolPerLTarget = double.tryParse(_mmolPerLTargetController.text); logBolus.mgPerDlCorrection = int.tryParse(_mgPerDlCorrectionController.text); logBolus.mmolPerLCorrection = double.tryParse(_mmolPerLCorrectionController.text); if (delayedBolus != null) { delayedBolus.mgPerDlCurrent = int.tryParse(_mgPerDlCurrentController.text); delayedBolus.mmolPerLCurrent = double.tryParse(_mmolPerLCurrentController.text); delayedBolus.mgPerDlTarget = int.tryParse(_mgPerDlTargetController.text); delayedBolus.mmolPerLTarget = double.tryParse(_mmolPerLTargetController.text); delayedBolus.mgPerDlCorrection = int.tryParse(_mgPerDlCorrectionController.text); delayedBolus.mmolPerLCorrection = double.tryParse(_mmolPerLCorrectionController.text); } } logBolus.logEntry.target = _logEntry; logBolus.meal.target = _meal; logBolus.rate.target = _rate; LogBolus.put(logBolus); if (delayedBolus != null) { delayedBolus.logEntry.target = _logEntry; delayedBolus.meal.target = _meal; delayedBolus.rate.target = _rate; LogBolus.put(delayedBolus); } Navigator.pop(context, ['${_isNew ? 'New' : ''} Bolus Saved', logBolus, delayedBolus]); } setState(() { _isSaving = false; }); } void handleCancelAction() { if (Settings.get().showConfirmationDialogOnCancel && ((_isNew && (_carbsController.text != '' || (_bolusType == BolusType.glucose && (_mgPerDlCurrentController.text != (_logEntry?.mgPerDl.toString() ?? '') || _mmolPerLCurrentController.text != (_logEntry?.mmolPerL.toString() ?? ''))) || _mgPerDlTargetController.text != Settings.targetMgPerDl.toString() || _mmolPerLTargetController.text != Settings.targetMmolPerL.toString() || _delayController.text != '' || _setManually || _notesController.text != '')) || (!_isNew && (double.tryParse(_unitsController.text) != _logBolus!.units || double.tryParse(_carbsController.text) != _logBolus!.carbs || int.tryParse(_mgPerDlCurrentController.text) != _logBolus!.mgPerDlCurrent || int.tryParse(_mgPerDlTargetController.text) != _logBolus!.mgPerDlTarget || int.tryParse(_mgPerDlCorrectionController.text) != _logBolus!.mgPerDlCorrection || double.tryParse(_mmolPerLCurrentController.text) != _logBolus!.mmolPerLCurrent || double.tryParse(_mmolPerLTargetController.text) != _logBolus!.mmolPerLTarget || double.tryParse(_mmolPerLCorrectionController.text) != _logBolus!.mmolPerLCorrection || int.tryParse(_delayController.text) != _logBolus!.delay || _setManually != _logBolus!.setManually || _notesController.text != (_logBolus!.notes ?? ''))))) { DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, ); } else { Navigator.pop(context); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(_isNew ? 'New Bolus' : 'Edit Bolus'), ), drawer: const Navigation(currentLocation: LogBolusDetailScreen.routeName), body: Scrollbar( controller: _scrollController, child: SingleChildScrollView( controller: _scrollController, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ FormWrapper( formState: _logBolusForm, fields: [ Row( children: [ Expanded( child: NumberFormField( label: 'Bolus Units', suffix: ' U', controller: _unitsController, step: Settings.insulinSteps, autoRoundToMultipleOfStep: true, onChanged: (value) { setState(() { _setManually = true; }); updateDelayedRatio(totalUnitsUpdate: value); }, ), ), Expanded( child: BooleanFormField( contentPadding: const EdgeInsets.only( left: 10.0, right: 10.0, top: 10.0), value: _setManually, label: 'set manually', onChanged: (value) { setState(() { _setManually = value; calculateBolus(); }); }, ), ), ], ), Row( children: [ Expanded( child: RadioListTile( title: const Text('for glucose'), groupValue: _bolusType, value: BolusType.glucose, onChanged: (_) { setState(() { _bolusType = BolusType.glucose; onChangeGlucose(); }); }), ), Expanded( child: RadioListTile( title: const Text('for meal'), groupValue: _bolusType, value: BolusType.meal, onChanged: (value) { setState(() { _bolusType = BolusType.meal; calculateBolus(); }); }), ), ], ), Column( children: _bolusType == BolusType.glucose ? [ Row( children: Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl || [ GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail ].contains(Settings.glucoseDisplayMode) ? [ Expanded( child: Padding( padding: const EdgeInsets.only(right: 5.0), child: NumberFormField( label: 'Current', suffix: 'mg/dl', controller: _mgPerDlCurrentController, onChanged: (_) => onChangeGlucose(), showSteppers: false, ), ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric( horizontal: 5.0), child: NumberFormField( label: 'Target', suffix: 'mg/dl', controller: _mgPerDlTargetController, onChanged: (_) => onChangeGlucose(), showSteppers: false, ), ), ), Expanded( child: Padding( padding: const EdgeInsets.only(left: 5.0), child: TextFormField( decoration: const InputDecoration( labelText: 'Correction', suffixText: 'mg/dl', ), controller: _mgPerDlCorrectionController, readOnly: true, ), ), ), ] : [], ), Padding( padding: EdgeInsets.only( top: [ GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail ].contains(Settings.glucoseDisplayMode) ? 10.0 : 0.0), child: Row( children: Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL || [ GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail ].contains(Settings.glucoseDisplayMode) ? [ Expanded( child: Padding( padding: const EdgeInsets.only( right: 5.0), child: NumberFormField( label: 'Current', suffix: 'mmol/l', controller: _mmolPerLCurrentController, onChanged: (_) => onChangeGlucose(), showSteppers: false, ), ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric( horizontal: 5.0), child: NumberFormField( label: 'Target', suffix: 'mmol/l', controller: _mmolPerLTargetController, onChanged: (_) => onChangeGlucose(), showSteppers: false, ), ), ), Expanded( child: Padding( padding: const EdgeInsets.only( left: 5.0), child: TextFormField( decoration: const InputDecoration( labelText: 'Correction', suffixText: 'mmol/l', ), controller: _mmolPerLCorrectionController, readOnly: true, ), ), ), ] : [], ), ), ] : [ Row( children: [ Expanded( child: AutoCompleteDropdownButton( controller: _mealController, selectedItem: _meal, label: 'Meal', items: _logMeals, onChanged: onSelectMeal, ), ), IconButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => _meal == null ? const LogMealDetailScreen() : LogMealDetailScreen( id: _meal!.id), ), ).then((result) { updateLogMeal(result?[1]); reload(message: result?[0]); }); }, icon: Icon( _meal == null ? Icons.add : Icons.edit), ), ], ), Padding( padding: const EdgeInsets.only(top: 10.0), child: NumberFormField( label: 'Carbs', suffix: Settings.nutritionMeasurementSuffix, controller: _carbsController, step: Settings.nutritionSteps, onChanged: (value) { _carbsController.text = (value ?? 0).toString(); calculateBolus(); }, ), ), ], ), Row( children: [ Expanded( child: TextFormField( decoration: const InputDecoration( labelText: 'Delayed Bolus Duration', suffixText: ' min', ), controller: _delayController, onChanged: (value) => setState(() {}), keyboardType: const TextInputType.numberWithOptions(), ), ), Expanded( child: Slider( label: '${_delayPercentage.floor().toString()}%', divisions: 100, value: _delayPercentage, min: 0, max: 100, onChanged: _delayController.text != '' ? (value) { updateDelayedRatio(percentageUpdate: value); } : null, ), ), const Text('%', textScaleFactor: 1.5), ], ), Row( children: (int.tryParse(_delayController.text) ?? 0) != 0 ? [ Expanded( child: Padding( padding: const EdgeInsets.only(right: 5.0), child: NumberFormField( label: 'Immediate Bolus', suffix: ' U', controller: _immediateUnitsController, max: double.tryParse(_unitsController.text), step: Settings.insulinSteps, readOnly: true, onChanged: (value) => updateDelayedRatio( immediateUnitsUpdate: value), ), ), ), Expanded( 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), ), ), ), ] : [], ), TextFormField( controller: _notesController, decoration: const InputDecoration( labelText: 'Notes', ), keyboardType: TextInputType.multiline, minLines: 2, maxLines: 5, ), ], ), ], ), ), ), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, onAction: _isSaving ? null : handleSaveAction, ), ); } }