import 'package:diameter/components/detail.dart'; import 'package:diameter/components/dialogs.dart'; import 'package:diameter/config.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/settings.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.dart'; import 'package:diameter/models/bolus.dart'; import 'package:diameter/models/bolus_profile.dart'; class BolusDetailScreen extends StatefulWidget { static const String routeName = '/bolus'; final BolusProfile bolusProfile; final Bolus? bolus; final TimeOfDay? suggestedStartTime; final TimeOfDay? suggestedEndTime; const BolusDetailScreen( {Key? key, required this.bolusProfile, this.bolus, this.suggestedStartTime, this.suggestedEndTime}) : super(key: key); @override _BolusDetailScreenState createState() => _BolusDetailScreenState(); } class _BolusDetailScreenState extends State { final GlobalKey _bolusForm = GlobalKey(); 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: ''); final _carbsController = TextEditingController(text: ''); final _mgPerDlController = TextEditingController(text: ''); final _mmolPerLController = TextEditingController(text: ''); @override void initState() { super.initState(); if (widget.suggestedStartTime != null) { _startTime = widget.suggestedStartTime!; } if (widget.suggestedEndTime != null) { _endTime = widget.suggestedEndTime!; } if (widget.bolus != null) { _startTime = TimeOfDay.fromDateTime(widget.bolus!.startTime); _endTime = TimeOfDay.fromDateTime(widget.bolus!.endTime); _unitsController.text = widget.bolus!.units.toString(); _carbsController.text = widget.bolus!.carbs.toString(); _mgPerDlController.text = widget.bolus!.mgPerDl.toString(); _mmolPerLController.text = widget.bolus!.mmolPerL.toString(); } updateStartTime(); updateEndTime(); } void updateStartTime() { _startTimeController.text = DateTimeUtils.displayTimeOfDay(_startTime); } void updateEndTime() { _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); } Future validateTimePeriod() async { String? error; List bolusRates = await Bolus.fetchAllForBolusProfile(widget.bolusProfile); // check for duplicates if (bolusRates .where((other) => (widget.bolus == null || widget.bolus!.objectId != other.objectId) && _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.bolus == null || widget.bolus!.objectId != other.objectId) && 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() async { if (_bolusForm.currentState!.validate()) { await validateTimePeriod().then((value) async { if (value != 'CANCEL') { bool isNew = widget.bolus == null; isNew ? await Bolus.save( startTime: DateTimeUtils.convertTimeOfDayToDateTime(_startTime), endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime), units: double.parse(_unitsController.text), bolusProfile: widget.bolusProfile.objectId!, carbs: double.parse(_carbsController.text), mgPerDl: int.tryParse(_mgPerDlController.text), mmolPerL: double.tryParse(_mmolPerLController.text), ) : await Bolus.update( widget.bolus!.objectId!, startTime: DateTimeUtils.convertTimeOfDayToDateTime(_startTime), endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime), units: double.tryParse(_unitsController.text), carbs: double.tryParse(_carbsController.text), mgPerDl: int.tryParse(_mgPerDlController.text), mmolPerL: double.parse(_mmolPerLController.text), ); Navigator.pop(context, '${isNew ? 'New' : ''} Bolus Rate saved'); } }); } } void handleCancelAction() { bool isNew = widget.bolus == null; if (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(widget.bolus!.startTime) != _startTime || TimeOfDay.fromDateTime(widget.bolus!.endTime) != _endTime || (double.tryParse(_unitsController.text) ?? 0) != widget.bolus!.units || (double.tryParse(_carbsController.text) ?? 0) != widget.bolus!.carbs || (double.tryParse(_mgPerDlController.text) ?? 0) != widget.bolus!.mgPerDl || (double.tryParse(_mmolPerLController.text) ?? 0) != widget.bolus!.mmolPerL)))) { Dialogs.showCancelConfirmationDialog( context: context, isNew: isNew, onSave: handleSaveAction, ); } else { Navigator.pop(context); } } void convertBetweenMgPerDlAndMmolPerL({GlucoseMeasurement? calculateFrom}) { int? mgPerDl; double? mmolPerL; if (calculateFrom != GlucoseMeasurement.mmolPerL && _mgPerDlController.text != '') { mgPerDl = int.tryParse(_mgPerDlController.text); } if (calculateFrom != GlucoseMeasurement.mgPerDl && _mmolPerLController.text != '') { mmolPerL = double.tryParse(_mmolPerLController.text); } if (mgPerDl != null && mmolPerL == null) { setState(() { _mmolPerLController.text = Utils.convertMgPerDlToMmolPerL(mgPerDl!).toString(); }); } if (mmolPerL != null && mgPerDl == null) { setState(() { _mgPerDlController.text = Utils.convertMmolPerLToMgPerDl(mmolPerL!).toString(); }); } } @override Widget build(BuildContext context) { bool isNew = widget.bolus == null; return Scaffold( appBar: AppBar( title: Text( '${isNew ? 'New' : 'Edit'} Bolus Rate for ${widget.bolusProfile.name}'), ), drawer: const Navigation(currentLocation: BolusDetailScreen.routeName), body: SingleChildScrollView( child: Column( children: [ StyledForm( formState: _bolusForm, fields: [ Row( children: [ Expanded( child: Padding( padding: const EdgeInsets.only(right: 5), child: StyledTimeOfDayFormField( label: 'Start Time', controller: _startTimeController, time: _startTime, onChanged: (newStartTime) { if (newStartTime != null) { setState(() { _startTime = newStartTime; }); updateStartTime(); } }, ), ), ), Expanded( child: Padding( padding: const EdgeInsets.only(left: 5), child: StyledTimeOfDayFormField( label: 'End Time', controller: _endTimeController, time: _endTime, onChanged: (newEndTime) { if (newEndTime != null) { setState(() { _endTime = newEndTime; }); updateEndTime(); } }, ), ), ), ], ), TextFormField( decoration: const InputDecoration( labelText: 'Units', suffixText: 'U', ), controller: _unitsController, keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: (value) { if (value!.trim().isEmpty) { return 'Empty amount of units'; } return null; }, ), TextFormField( decoration: InputDecoration( labelText: 'per carbs', suffixText: nutritionMeasurement == NutritionMeasurement.grams ? 'g' : nutritionMeasurement == NutritionMeasurement.ounces ? 'oz' : '', ), controller: _carbsController, keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: (value) { if (value!.trim().isEmpty) { return 'How many carbs does the rate make up for?'; } return null; }, ), Row( children: [ glucoseMeasurement == GlucoseMeasurement.mgPerDl || glucoseDisplayMode == GlucoseDisplayMode.both || glucoseDisplayMode == GlucoseDisplayMode.bothForDetail ? Expanded( child: TextFormField( decoration: const InputDecoration( labelText: 'per mg/dl', suffixText: 'mg/dl', ), controller: _mgPerDlController, onChanged: (_) => convertBetweenMgPerDlAndMmolPerL( calculateFrom: GlucoseMeasurement.mgPerDl), keyboardType: const TextInputType.numberWithOptions(), validator: (value) { if (value!.trim().isEmpty && _mmolPerLController.text.trim().isEmpty) { return 'How many mg/dl does the rate make up for?'; } return null; }, ), ) : Container(), glucoseDisplayMode == GlucoseDisplayMode.both || glucoseDisplayMode == GlucoseDisplayMode.bothForDetail ? IconButton( onPressed: () => convertBetweenMgPerDlAndMmolPerL( calculateFrom: GlucoseMeasurement.mmolPerL), icon: const Icon(Icons.calculate), ) : Container(), glucoseMeasurement == GlucoseMeasurement.mmolPerL || glucoseDisplayMode == GlucoseDisplayMode.both || glucoseDisplayMode == GlucoseDisplayMode.bothForDetail ? Expanded( child: TextFormField( decoration: const InputDecoration( labelText: 'per mmol/l', suffixText: 'mmol/l', ), controller: _mmolPerLController, onChanged: (_) => convertBetweenMgPerDlAndMmolPerL( calculateFrom: GlucoseMeasurement.mmolPerL), keyboardType: const TextInputType.numberWithOptions( decimal: true), validator: (value) { if (value!.trim().isEmpty && _mgPerDlController.text.trim().isEmpty) { return 'How many mmol/l does rhe rate make up for?'; } return null; }, ), ) : Container(), glucoseDisplayMode == GlucoseDisplayMode.both || glucoseDisplayMode == GlucoseDisplayMode.bothForDetail ? IconButton( onPressed: () => convertBetweenMgPerDlAndMmolPerL( calculateFrom: GlucoseMeasurement.mgPerDl), icon: const Icon(Icons.calculate), ) : Container(), ], ), ], ), ], ), ), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, onSave: handleSaveAction, ), ); } }