import 'dart:math'; import 'package:diameter/components/detail.dart'; import 'package:diameter/components/dialogs.dart'; import 'package:diameter/components/dropdown.dart'; import 'package:diameter/components/forms.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.getAllForEntry(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(); } 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(); }); } void updateDelayedRatio() { if (_unitsController.text != '') { setState(() { _delayedUnitsController.text = ((double.tryParse(_unitsController.text) ?? 0) * _delayPercentage / 100) .toString(); _immediateUnitsController.text = ((double.tryParse(_unitsController.text) ?? 0) * (100 - _delayPercentage) / 100) .toString(); }); } } void onSelectMeal(LogMeal? meal) { updateLogMeal(meal); if (meal != null && meal.carbsPerPortion != null) { setState(() { _carbsController.text = meal.carbsPerPortion.toString(); onChangeCarbs(); }); } } void onChangeCarbs() { setState(() { if (_rate != null && !_setManually) { _unitsController.text = ((double.tryParse(_carbsController.text) ?? 0) / (_rate!.carbs / _rate!.units)) .toString(); if (_unitsController.text != '') { _delayedUnitsController.text = ((double.tryParse(_unitsController.text) ?? 0) * _delayPercentage / 100) .toString(); _immediateUnitsController.text = ((double.tryParse(_unitsController.text) ?? 0) * (100 - _delayPercentage) / 100) .toString(); } } }); } void onChangeGlucose({GlucoseMeasurement? calculateFrom}) { int? mgPerDlCurrent; int? mgPerDlTarget; int? mgPerDlCorrection; double? mmolPerLCurrent; double? mmolPerLTarget; double? mmolPerLCorrection; if (calculateFrom != GlucoseMeasurement.mmolPerL && _mgPerDlCurrentController.text != '' && _mgPerDlTargetController.text != '') { mgPerDlCurrent = int.tryParse(_mgPerDlCurrentController.text); mgPerDlTarget = int.tryParse(_mgPerDlTargetController.text); mgPerDlCorrection = max((mgPerDlCurrent ?? 0) - (mgPerDlTarget ?? 0), 0); } if (calculateFrom != GlucoseMeasurement.mgPerDl && _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) { _unitsController.text = ((mgPerDlCorrection ?? 0) / ((_rate!.mgPerDl ?? 0) / _rate!.units)) .toString(); } }); } 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) { _unitsController.text = ((mmolPerLCorrection ?? 0) / ((_rate!.mmolPerL ?? 0) / _rate!.units)) .toString(); } }); } } 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 ?? ''))))) { Dialogs.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: TextFormField( decoration: const InputDecoration( labelText: 'Bolus Units', suffixText: ' U', ), controller: _unitsController, onChanged: (_) { setState(() { _setManually = true; }); updateDelayedRatio(); }, keyboardType: const TextInputType.numberWithOptions( decimal: true), ), ), 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; }); }, ), ), ], ), Row( children: [ Expanded( child: RadioListTile( title: const Text('for glucose'), groupValue: _bolusType, value: BolusType.glucose, onChanged: (_) { setState(() { _bolusType = BolusType.glucose; }); }), ), Expanded( child: RadioListTile( title: const Text('for meal'), groupValue: _bolusType, value: BolusType.meal, onChanged: (value) { setState(() { _bolusType = BolusType.meal; }); }), ), ], ), 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: TextFormField( decoration: const InputDecoration( labelText: 'Current', suffixText: 'mg/dl', ), controller: _mgPerDlCurrentController, onChanged: (_) async { await Future.delayed( const Duration(seconds: 1)); onChangeGlucose( calculateFrom: GlucoseMeasurement .mgPerDl); }, keyboardType: const TextInputType .numberWithOptions(), ), ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric( horizontal: 5.0), child: TextFormField( decoration: const InputDecoration( labelText: 'Target', suffixText: 'mg/dl', ), controller: _mgPerDlTargetController, onChanged: (_) async { await Future.delayed( const Duration(seconds: 1)); onChangeGlucose( calculateFrom: GlucoseMeasurement .mgPerDl); }, keyboardType: const TextInputType .numberWithOptions(), ), ), ), Expanded( child: Padding( padding: const EdgeInsets.only(left: 5.0), child: TextFormField( decoration: const InputDecoration( labelText: 'Correction', suffixText: 'mg/dl', ), controller: _mgPerDlCorrectionController, readOnly: true, ), ), ), [ GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail ].contains(Settings.glucoseDisplayMode) ? IconButton( onPressed: () => onChangeGlucose( calculateFrom: GlucoseMeasurement .mmolPerL), icon: const Icon(Icons.calculate), ) : Container(), ] : [], ), Row( children: Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL || [ GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail ].contains(Settings.glucoseDisplayMode) ? [ Expanded( child: Padding( padding: const EdgeInsets.only(right: 5.0), child: TextFormField( decoration: const InputDecoration( labelText: 'Current', suffixText: 'mmol/l', ), controller: _mmolPerLCurrentController, onChanged: (_) async { await Future.delayed( const Duration(seconds: 1)); onChangeGlucose( calculateFrom: GlucoseMeasurement .mmolPerL); }, keyboardType: const TextInputType .numberWithOptions(), ), ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric( horizontal: 5.0), child: TextFormField( decoration: const InputDecoration( labelText: 'Target', suffixText: 'mmol/l', ), controller: _mmolPerLTargetController, onChanged: (_) async { await Future.delayed( const Duration(seconds: 1)); onChangeGlucose( calculateFrom: GlucoseMeasurement .mmolPerL); }, keyboardType: const TextInputType .numberWithOptions(), ), ), ), Expanded( child: Padding( padding: const EdgeInsets.only(left: 5.0), child: TextFormField( decoration: const InputDecoration( labelText: 'Correction', suffixText: 'mmol/l', ), controller: _mmolPerLCorrectionController, readOnly: true, ), ), ), [ GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail ].contains(Settings.glucoseDisplayMode) ? IconButton( onPressed: () => onChangeGlucose( calculateFrom: GlucoseMeasurement .mgPerDl), icon: const Icon(Icons.calculate), ) : Container(), ] : [], ), ] : [ 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: TextFormField( decoration: InputDecoration( labelText: 'Carbs', suffixText: Settings.nutritionMeasurementSuffix, ), controller: _carbsController, onChanged: (_) => onChangeCarbs(), keyboardType: const TextInputType.numberWithOptions( decimal: true), ), ), ], ), TextFormField( decoration: const InputDecoration( labelText: 'Delayed Bolus Duration', suffixText: ' min', ), controller: _delayController, onChanged: (value) => setState(() {}), keyboardType: const TextInputType.numberWithOptions(), ), (int.tryParse(_delayController.text) ?? 0) != 0 ? Slider( label: '${_delayPercentage.floor().toString()}%', divisions: 100, value: _delayPercentage, min: 0, max: 100, onChanged: _delayController.text != '' ? (value) { setState(() { _delayPercentage = value; }); updateDelayedRatio(); } : null, ) : Container(), Row( children: (int.tryParse(_delayController.text) ?? 0) != 0 ? [ Expanded( child: Padding( padding: const EdgeInsets.only(right: 5.0), child: TextFormField( decoration: const InputDecoration( labelText: 'Immediate Bolus', suffixText: ' U', ), controller: _immediateUnitsController, readOnly: true, enabled: (int.tryParse(_delayController.text) ?? 0) != 0, ), ), ), Expanded( child: Padding( padding: const EdgeInsets.only(left: 5.0), child: TextFormField( decoration: const InputDecoration( labelText: 'Delayed Bolus', suffixText: ' U', ), controller: _delayedUnitsController, readOnly: true, enabled: (int.tryParse(_delayController.text) ?? 0) != 0, ), ), ), ] : [], ), TextFormField( controller: _notesController, decoration: const InputDecoration( labelText: 'Notes', ), keyboardType: TextInputType.multiline, minLines: 2, maxLines: 5, ), ], ), ], ), ), ), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, onSave: _isSaving ? null : handleSaveAction, ), ); } }