import 'package:diameter/components/detail.dart'; import 'package:diameter/components/forms/number_form_field.dart'; import 'package:diameter/components/forms/time_of_day_form_field.dart'; import 'package:diameter/utils/dialog_utils.dart'; import 'package:diameter/models/settings.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/utils/date_time_utils.dart'; import 'package:diameter/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:diameter/components/forms/form_wrapper.dart'; import 'package:diameter/models/bolus.dart'; import 'package:diameter/models/bolus_profile.dart'; class BolusDetailScreen extends StatefulWidget { static const String routeName = '/bolus'; final int bolusProfileId; final int id; final TimeOfDay? suggestedStartTime; final TimeOfDay? suggestedEndTime; const BolusDetailScreen( {Key? key, this.bolusProfileId = 0, this.id = 0, this.suggestedStartTime, this.suggestedEndTime}) : super(key: key); @override _BolusDetailScreenState createState() => _BolusDetailScreenState(); } class _BolusDetailScreenState extends State { Bolus? _bolus; bool _isNew = true; bool _isSaving = false; bool _isFinalRate = true; final GlobalKey _bolusForm = GlobalKey(); final ScrollController _scrollController = ScrollController(); TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0); TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0); final _startTimeController = TextEditingController(text: ''); final _endTimeController = TextEditingController(text: ''); final _unitsController = TextEditingController(text: Utils.toStringMatchingTemplateFractionPrecision(0, Settings.insulinSteps)); final _carbsController = TextEditingController(text: Utils.toStringMatchingTemplateFractionPrecision(0, Settings.nutritionSteps)); final _mgPerDlController = TextEditingController(text: '0'); final _mmolPerLController = TextEditingController(text: Utils.toStringMatchingTemplateFractionPrecision(0, Settings.mmolPerLSteps)); @override void initState() { super.initState(); reload(); if (widget.suggestedStartTime != null) { _startTime = widget.suggestedStartTime!; } if (widget.suggestedEndTime != null) { _endTime = widget.suggestedEndTime!; } if (_bolus != null) { _startTime = TimeOfDay.fromDateTime(_bolus!.startTime); _endTime = TimeOfDay.fromDateTime(_bolus!.endTime); _unitsController.text = _bolus!.units.toString(); _carbsController.text = _bolus!.carbs.toString(); _mgPerDlController.text = (_bolus!.mgPerDl ?? '').toString(); _mmolPerLController.text = (_bolus!.mmolPerL ?? '').toString(); } _startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime); _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); } @override void dispose() { _scrollController.dispose(); _startTimeController.dispose(); _endTimeController.dispose(); _unitsController.dispose(); _carbsController.dispose(); _mgPerDlController.dispose(); _mmolPerLController.dispose(); super.dispose(); } void reload({String? message}) { if (widget.id != 0) { setState(() { _bolus = Bolus.get(widget.id); }); } _isNew = _bolus == null; setState(() { if (message != null) { var snackBar = SnackBar( content: Text(message), duration: const Duration(seconds: 2), ); ScaffoldMessenger.of(context) ..removeCurrentSnackBar() ..showSnackBar(snackBar); } }); } void updateStartTime(TimeOfDay? value) { if (value != null) { setState(() { _startTime = value; _startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime); }); } } void updateEndTime(TimeOfDay? value) { if (value != null) { setState(() { _endTime = value; _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); _isFinalRate = widget.suggestedEndTime == null || _endTime == widget.suggestedEndTime!; }); } } Future validateTimePeriod() async { String? error; List bolusRates = Bolus.getAllForProfile(widget.bolusProfileId); // check for duplicates if (bolusRates .where((other) => widget.id != other.id && _startTime.hour == other.startTime.hour && _startTime.minute == other.startTime.minute) .isNotEmpty) { error = 'There\'s already a rate with this start time.'; } if (bolusRates .where((other) => (widget.id != other.id) && DateTimeUtils.convertTimeOfDayToDateTime(_startTime) .isBefore(other.startTime) && DateTimeUtils.convertTimeOfDayToDateTime(_endTime) .isAfter(other.startTime)) .isNotEmpty) { error = 'This rate\'s time period overlaps with another one.'; } return error == null ? null : showDialog( context: context, builder: (BuildContext context) { return AlertDialog( content: Text(error!), actions: [ TextButton( onPressed: () => Navigator.pop(context, 'CANCEL'), child: const Text('GO BACK TO EDITING'), ), ElevatedButton( onPressed: () => Navigator.pop(context, 'CONFIRM'), child: const Text('SAVE AS IS'), ), ], ); }); } void handleSaveAction({bool next = true}) async { setState(() { _isSaving = true; }); if (_bolusForm.currentState!.validate()) { await validateTimePeriod().then((value) async { if (value != 'CANCEL') { Bolus bolus = Bolus( id: widget.id, startTime: DateTimeUtils.convertTimeOfDayToDateTime(_startTime), endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime), units: double.tryParse(_unitsController.text) ?? 0, carbs: double.tryParse(_carbsController.text) ?? 0, mgPerDl: int.tryParse(_mgPerDlController.text), mmolPerL: double.tryParse(_mmolPerLController.text), ); bolus.bolusProfile.targetId = widget.bolusProfileId; Bolus.put(bolus); if (next) { Navigator.push( context, MaterialPageRoute( builder: (context) { return BolusDetailScreen( bolusProfileId: widget.bolusProfileId, suggestedStartTime: _endTime, suggestedEndTime: widget.suggestedEndTime, ); }, ), ).then((result) { Navigator.pop( context, ['New Bolus Rate${result[1] != null ? 's' : ''} saved', bolus] + [result[1]], ); }); } else { Navigator.pop( context, ['${_isNew ? 'New' : ''} Bolus Rate saved', bolus]); } } }); } setState(() { _isSaving = false; }); } void handleCancelAction() { if (Settings.get().showConfirmationDialogOnCancel && ((_isNew && (_startTime.hour != (widget.suggestedStartTime?.hour ?? 0) || _endTime.hour != (widget.suggestedEndTime?.hour ?? 0) || _startTime.minute != (widget.suggestedStartTime?.minute ?? 0) || _endTime.minute != (widget.suggestedEndTime?.minute ?? 0) || (double.tryParse(_unitsController.text) ?? 0) != 0.0 || (double.tryParse(_carbsController.text) ?? 0) != 0.0 || (int.tryParse(_mgPerDlController.text) ?? 0) != 0 || (double.tryParse(_mmolPerLController.text) ?? 0) != 0.0)) || (!_isNew && (TimeOfDay.fromDateTime(_bolus!.startTime) != _startTime || TimeOfDay.fromDateTime(_bolus!.endTime) != _endTime || (double.tryParse(_unitsController.text) ?? 0) != _bolus!.units || (double.tryParse(_carbsController.text) ?? 0) != _bolus!.carbs || (double.tryParse(_mgPerDlController.text) ?? 0) != _bolus!.mgPerDl || (double.tryParse(_mmolPerLController.text) ?? 0) != _bolus!.mmolPerL)))) { DialogUtils.showCancelConfirmationDialog( context: context, isNew: _isNew, onSave: handleSaveAction, ); } else { Navigator.pop(context); } } void convertBetweenMgPerDlAndMmolPerL(double? value) async { if (value != null) { if (Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl && _mgPerDlController.text != '') { _mgPerDlController.text = value.toInt().toString(); setState(() { _mmolPerLController.text = Utils.convertMgPerDlToMmolPerL(value.toInt()).toString(); }); } if (Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL && _mmolPerLController.text != '') { _mmolPerLController.text = Utils.toStringMatchingTemplateFractionPrecision( value, Settings.mmolPerLSteps); setState(() { _mgPerDlController.text = Utils.convertMmolPerLToMgPerDl(value.toDouble()).toString(); }); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( '${_isNew ? 'New' : 'Edit'} Bolus Rate for ${BolusProfile.get(widget.bolusProfileId)?.name}'), ), drawer: const Navigation(currentLocation: BolusDetailScreen.routeName), body: Scrollbar( controller: _scrollController, child: SingleChildScrollView( controller: _scrollController, child: Column( children: [ FormWrapper( formState: _bolusForm, fields: [ Row( children: [ Expanded( child: Padding( padding: const EdgeInsets.only(right: 5), child: TimeOfDayFormField( label: 'Start Time', controller: _startTimeController, time: _startTime, onChanged: updateStartTime, ), ), ), Expanded( child: Padding( padding: const EdgeInsets.only(left: 5), child: TimeOfDayFormField( label: 'End Time', controller: _endTimeController, time: _endTime, onChanged: updateEndTime, ), ), ), ], ), NumberFormField( controller: _unitsController, label: 'Units', suffix: 'U', autoRoundToMultipleOfStep: true, step: Settings.insulinSteps, onChanged: (value) { if (value != null) { _unitsController.text = Utils.toStringMatchingTemplateFractionPrecision( value, Settings.insulinSteps); } }, ), NumberFormField( controller: _carbsController, label: 'per carbs', suffix: Settings.nutritionMeasurementSuffix, autoRoundToMultipleOfStep: true, step: Settings.nutritionSteps, onChanged: (value) { if (value != null) { _carbsController.text = Utils.toStringMatchingTemplateFractionPrecision( value, Settings.nutritionSteps); } }, ), Row( children: [ Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl || Settings.glucoseDisplayMode == GlucoseDisplayMode.both || Settings.glucoseDisplayMode == GlucoseDisplayMode.bothForDetail ? Expanded( flex: Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl ? 2 : 1, child: NumberFormField( label: 'per mg/dl', suffix: 'mg/dl', readOnly: Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL, showSteppers: Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl, controller: _mgPerDlController, onChanged: convertBetweenMgPerDlAndMmolPerL, ), ) : Container(), Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL || [ GlucoseDisplayMode.both, GlucoseDisplayMode.bothForDetail ].contains(Settings.glucoseDisplayMode) ? Expanded( flex: Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL ? 2 : 1, child: NumberFormField( label: 'per mmol/l', suffix: 'mmol/l', readOnly: Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl, showSteppers: Settings.glucoseMeasurement == GlucoseMeasurement.mmolPerL, controller: _mmolPerLController, step: Settings.mmolPerLSteps, onChanged: convertBetweenMgPerDlAndMmolPerL, ), ) : Container(), ], ), ], ), ], ), ), ), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, onAction: _isSaving ? null : () => handleSaveAction(next: !_isFinalRate), onMiddleAction: _isSaving || _isFinalRate ? null : () => handleSaveAction(next: false), actionText: _isFinalRate ? 'SAVE & CLOSE' : 'NEXT', middleActionText: 'SAVE & CLOSE', ), ); } }