From db023a94cfee027829808db1e2952ff3fff38fa0 Mon Sep 17 00:00:00 2001 From: spinel Date: Tue, 26 Oct 2021 01:11:58 +0200 Subject: [PATCH] implement time period validation for bolus/basal profiles --- lib/screens/basal/basal_detail.dart | 56 +++++----- lib/screens/basal/basal_list.dart | 110 +++++++++++++----- lib/screens/basal/basal_profile_detail.dart | 99 +++++++++-------- lib/screens/bolus/bolus_detail.dart | 78 +++++++------ lib/screens/bolus/bolus_list.dart | 117 +++++++++++++++----- lib/screens/bolus/bolus_profile_detail.dart | 96 ++++++++-------- lib/screens/log/log_entry.dart | 19 ++-- lib/screens/log/log_entry_form.dart | 72 ++++++++---- lib/utils/date_time_utils.dart | 2 + 9 files changed, 407 insertions(+), 242 deletions(-) diff --git a/lib/screens/basal/basal_detail.dart b/lib/screens/basal/basal_detail.dart index 0ab2bab..e686695 100644 --- a/lib/screens/basal/basal_detail.dart +++ b/lib/screens/basal/basal_detail.dart @@ -4,7 +4,6 @@ import 'package:diameter/config.dart'; import 'package:diameter/navigation.dart'; import 'package:diameter/utils/date_time_utils.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:diameter/components/forms.dart'; import 'package:diameter/models/basal.dart'; import 'package:diameter/models/basal_profile.dart'; @@ -116,33 +115,40 @@ class _BasalDetailScreenState extends State { Row( children: [ Expanded( - child: StyledTimeOfDayFormField( - label: 'Start Time', - controller: _startTimeController, - time: _startTime, - onChanged: (newStartTime) { - if (newStartTime != null) { - setState(() { - _startTime = newStartTime; - }); - updateStartTime(); - } - }, + // TODO fix handling of time zones! + 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: StyledTimeOfDayFormField( - label: 'End Time', - controller: _endTimeController, - time: _endTime, - onChanged: (newEndTime) { - if (newEndTime != null) { - setState(() { - _endTime = newEndTime; - }); - updateEndTime(); - } - }, + 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(); + } + }, + ), ), ), ], diff --git a/lib/screens/basal/basal_list.dart b/lib/screens/basal/basal_list.dart index 14c014d..f02d1eb 100644 --- a/lib/screens/basal/basal_list.dart +++ b/lib/screens/basal/basal_list.dart @@ -1,5 +1,6 @@ import 'package:diameter/components/dialogs.dart'; import 'package:diameter/config.dart'; +import 'package:diameter/utils/date_time_utils.dart'; import 'package:flutter/material.dart'; import 'package:diameter/components/progress_indicator.dart'; import 'package:diameter/models/basal.dart'; @@ -64,6 +65,44 @@ class _BasalListScreenState extends State { } } + String? checkBasalValidity(List basalRates, int index) { + Basal basal = basalRates[index]; + + // check for gaps + if (index == 0 && + (basal.startTime.toLocal().hour != 0 || basal.startTime.minute != 0)) { + return 'First Basal of the day needs to start at 00:00'; + } + + if (index > 0) { + var lastEndTime = basalRates[index - 1].endTime; + if (basal.startTime.isAfter(lastEndTime)) { + return 'There\'s a time gap between this and the previous rate'; + } + } + + if (index == basalRates.length - 1 && + (basal.endTime.toLocal().hour != 0 || basal.endTime.minute != 0)) { + return 'Last Basal of the day needs to end at 00:00'; + } + + // check for duplicates + + if (basalRates + .where((other) => basal != other && basal.startTime == other.startTime) + .isNotEmpty) { + return 'There are multiple rates with this start time'; + } + + if (basalRates + .where((other) => + basal.startTime.isBefore(other.startTime) && + basal.endTime.isAfter(other.startTime)) + .isNotEmpty) { + return 'This rate\'s time period overlaps with another one'; + } + } + @override void initState() { super.initState(); @@ -73,47 +112,60 @@ class _BasalListScreenState extends State { @override Widget build(BuildContext context) { return SingleChildScrollView( - padding: const EdgeInsets.only(top: 10.0), child: Column( children: [ FutureBuilder>( future: widget.basalProfile!.basalRates, builder: (context, snapshot) { return ViewWithProgressIndicator( - // TODO: add warning if time period is missing or has multiple rates snapshot: snapshot, child: snapshot.data == null || snapshot.data!.isEmpty ? const Padding( padding: EdgeInsets.all(10.0), child: Text('No Basal Rates for this Profile'), ) - : ListBody( - children: [ - DataTable( - columnSpacing: 10.0, - showCheckboxColumn: false, - rows: snapshot.data != null - ? snapshot.data!.map((basal) { - return DataRow( - cells: basal.asDataTableCells([ - IconButton( - icon: const Icon(Icons.edit), - iconSize: 16.0, - onPressed: () => - handleEditAction(basal)), - IconButton( - icon: const Icon(Icons.delete), - iconSize: 16.0, - onPressed: () => - handleDeleteAction(basal), - ), - ]), - ); - }).toList() - : [], - columns: Basal.asDataTableColumns(), - ), - ], + : ListView.builder( + shrinkWrap: true, + itemCount: + snapshot.data != null ? snapshot.data!.length : 0, + itemBuilder: (context, index) { + final basal = snapshot.data![index]; + final error = + checkBasalValidity(snapshot.data!, index); + return ListTile( + tileColor: + error != null ? Colors.red.shade100 : null, + onTap: () { + handleEditAction(basal); + }, + title: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Text( + '${DateTimeUtils.displayTime(basal.startTime)} - ${DateTimeUtils.displayTime(basal.endTime)}')), + const Spacer(), + Expanded(child: Text('${basal.units} U')), + ], + ), + subtitle: error != null + ? Text(error, + style: const TextStyle(color: Colors.red)) + : Container(), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => handleDeleteAction(basal), + ), + ], + ), + ); + }, ), ); }, diff --git a/lib/screens/basal/basal_profile_detail.dart b/lib/screens/basal/basal_profile_detail.dart index 8d3768a..4f1c4e5 100644 --- a/lib/screens/basal/basal_profile_detail.dart +++ b/lib/screens/basal/basal_profile_detail.dart @@ -227,7 +227,7 @@ class _BasalProfileDetailScreenState extends State { Widget build(BuildContext context) { bool isNew = widget.basalProfile == null; return DefaultTabController( - length: 2, + length: isNew ? 1 : 2, child: Builder(builder: (BuildContext context) { final TabController tabController = DefaultTabController.of(context)!; tabController.addListener(() { @@ -235,6 +235,55 @@ class _BasalProfileDetailScreenState extends State { renderTabButtons(tabController.index); } }); + List tabs = [ + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StyledForm( + formState: _basalProfileForm, + fields: [ + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty title'; + } + return null; + }, + ), + TextFormField( + keyboardType: TextInputType.multiline, + controller: _notesController, + decoration: const InputDecoration( + labelText: 'Notes', + suffixText: '', + alignLabelWithHint: true, + ), + ), + StyledBooleanFormField( + value: _active, + onChanged: (value) { + setState(() { + _active = value; + }); + }, + label: 'active', + ), + ], + ), + ], + ), + ), + ]; + + if (!isNew) { + tabs.add(BasalListScreen(basalProfile: widget.basalProfile)); + } + return Scaffold( appBar: AppBar( title: @@ -251,53 +300,7 @@ class _BasalProfileDetailScreenState extends State { ), drawer: const Navigation( currentLocation: BasalProfileDetailScreen.routeName), - body: TabBarView( - children: [ - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StyledForm( - formState: _basalProfileForm, - fields: [ - TextFormField( - controller: _nameController, - decoration: const InputDecoration( - labelText: 'Name', - ), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty title'; - } - return null; - }, - ), - TextFormField( - keyboardType: TextInputType.multiline, - controller: _notesController, - decoration: const InputDecoration( - labelText: 'Notes', - suffixText: '', - alignLabelWithHint: true, - ), - ), - StyledBooleanFormField( - value: _active, - onChanged: (value) { - setState(() { - _active = value; - }); - }, - label: 'active', - ), - ], - ), - ], - ), - ), - BasalListScreen(basalProfile: widget.basalProfile), - ], - ), + body: TabBarView(children: tabs), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, onSave: handleSaveAction, diff --git a/lib/screens/bolus/bolus_detail.dart b/lib/screens/bolus/bolus_detail.dart index 83a37c1..2f24da0 100644 --- a/lib/screens/bolus/bolus_detail.dart +++ b/lib/screens/bolus/bolus_detail.dart @@ -6,11 +6,9 @@ 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:flutter/services.dart'; import 'package:diameter/components/forms.dart'; import 'package:diameter/models/bolus.dart'; import 'package:diameter/models/bolus_profile.dart'; -import 'package:flutter/widgets.dart'; class BolusDetailScreen extends StatefulWidget { static const String routeName = '/bolus'; @@ -31,12 +29,12 @@ class _BolusDetailScreenState extends State { TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0); TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0); - final _startTimeController = TextEditingController(); - final _endTimeController = TextEditingController(); - final _unitsController = TextEditingController(); - final _carbsController = TextEditingController(); - final _mgPerDlController = TextEditingController(); - final _mmolPerLController = TextEditingController(); + 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() { @@ -136,7 +134,6 @@ class _BolusDetailScreenState extends State { } void convertBetweenMgPerDlAndMmolPerL({GlucoseMeasurement? calculateFrom}) { - // TODO figure out why this isnt happening automatically int? mgPerDl; double? mmolPerL; @@ -182,33 +179,40 @@ class _BolusDetailScreenState extends State { Row( children: [ Expanded( - child: StyledTimeOfDayFormField( - label: 'Start Time', - controller: _startTimeController, - time: _startTime, - onChanged: (newStartTime) { - if (newStartTime != null) { - setState(() { - _startTime = newStartTime; - }); - updateStartTime(); - } - }, + child: Padding( + padding: const EdgeInsets.only(right: 5), + // TODO fix handling of time zones! + child: StyledTimeOfDayFormField( + label: 'Start Time', + controller: _startTimeController, + time: _startTime, + onChanged: (newStartTime) { + if (newStartTime != null) { + setState(() { + _startTime = newStartTime; + }); + updateStartTime(); + } + }, + ), ), ), Expanded( - child: StyledTimeOfDayFormField( - label: 'End Time', - controller: _endTimeController, - time: _endTime, - onChanged: (newEndTime) { - if (newEndTime != null) { - setState(() { - _endTime = newEndTime; - }); - updateEndTime(); - } - }, + 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(); + } + }, + ), ), ), ], @@ -262,7 +266,9 @@ class _BolusDetailScreenState extends State { ), controller: _mgPerDlController, onChanged: (_) => - convertBetweenMgPerDlAndMmolPerL, + convertBetweenMgPerDlAndMmolPerL( + calculateFrom: + GlucoseMeasurement.mgPerDl), keyboardType: const TextInputType.numberWithOptions(), validator: (value) { @@ -296,7 +302,9 @@ class _BolusDetailScreenState extends State { ), controller: _mmolPerLController, onChanged: (_) => - convertBetweenMgPerDlAndMmolPerL, + convertBetweenMgPerDlAndMmolPerL( + calculateFrom: + GlucoseMeasurement.mmolPerL), keyboardType: const TextInputType.numberWithOptions( decimal: true), diff --git a/lib/screens/bolus/bolus_list.dart b/lib/screens/bolus/bolus_list.dart index 854825d..c7bd822 100644 --- a/lib/screens/bolus/bolus_list.dart +++ b/lib/screens/bolus/bolus_list.dart @@ -1,5 +1,7 @@ import 'package:diameter/components/dialogs.dart'; import 'package:diameter/config.dart'; +import 'package:diameter/settings.dart'; +import 'package:diameter/utils/date_time_utils.dart'; import 'package:flutter/material.dart'; import 'package:diameter/components/progress_indicator.dart'; import 'package:diameter/models/bolus.dart'; @@ -64,6 +66,44 @@ class _BolusListScreenState extends State { } } + String? checkBolusValidity(List bolusRates, int index) { + Bolus bolus = bolusRates[index]; + + // check for gaps + if (index == 0 && + (bolus.startTime.toLocal().hour != 0 || bolus.startTime.minute != 0)) { + return 'First Bolus of the day needs to start at 00:00'; + } + + if (index > 0) { + var lastEndTime = bolusRates[index - 1].endTime; + if (bolus.startTime.isAfter(lastEndTime)) { + return 'There\'s a time gap between this and the previous rate'; + } + } + + if (index == bolusRates.length - 1 && + (bolus.endTime.toLocal().hour != 0 || bolus.endTime.minute != 0)) { + return 'Last Bolus of the day needs to end at 00:00'; + } + + // check for duplicates + + if (bolusRates + .where((other) => bolus != other && bolus.startTime == other.startTime) + .isNotEmpty) { + return 'There are multiple rates with this start time'; + } + + if (bolusRates + .where((other) => + bolus.startTime.isBefore(other.startTime) && + bolus.endTime.isAfter(other.startTime)) + .isNotEmpty) { + return 'This rate\'s time period overlaps with another one'; + } + } + @override void initState() { super.initState(); @@ -80,41 +120,58 @@ class _BolusListScreenState extends State { future: widget.bolusProfile!.bolusRates, builder: (context, snapshot) { return ViewWithProgressIndicator( - // TODO: add warning if time period is missing or has multiple rates snapshot: snapshot, child: snapshot.data == null || snapshot.data!.isEmpty ? const Padding( padding: EdgeInsets.all(10.0), - child: Text('No Bolus Rates for this Profile'), + child: Text('No Basal Rates for this Profile'), ) - : ListBody( - children: [ - DataTable( - columnSpacing: 10.0, - showCheckboxColumn: false, - rows: snapshot.data != null - ? snapshot.data!.map((bolus) { - return DataRow( - cells: bolus.asDataTableCells( - [ - IconButton( - icon: const Icon(Icons.edit), - iconSize: 16.0, - onPressed: () => - handleEditAction(bolus)), - IconButton( - icon: const Icon(Icons.delete), - iconSize: 16.0, - onPressed: () => - handleDeleteAction(bolus)), - ], - ), - ); - }).toList() - : [], - columns: Bolus.asDataTableColumns(), - ), - ], + : ListView.builder( + shrinkWrap: true, + itemCount: + snapshot.data != null ? snapshot.data!.length : 0, + itemBuilder: (context, index) { + final bolus = snapshot.data![index]; + final error = + checkBolusValidity(snapshot.data!, index); + return ListTile( + tileColor: + error != null ? Colors.red.shade100 : null, + onTap: () { + handleEditAction(bolus); + }, + title: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Text( + '${DateTimeUtils.displayTime(bolus.startTime)} - ${DateTimeUtils.displayTime(bolus.endTime)}')), + // TODO: style this + Expanded( + child: Text( + '${bolus.units} U per ${bolus.carbs}${nutritionMeasurement == NutritionMeasurement.grams ? ' g' : ' oz'} carbs/${glucoseMeasurement == GlucoseMeasurement.mgPerDl ? bolus.mgPerDl : bolus.mmolPerL} ${glucoseMeasurement == GlucoseMeasurement.mgPerDl ? 'mg/dl' : 'mmol/l'}', + style: const TextStyle(fontSize: 12.0)), + ), + ], + ), + subtitle: error != null + ? Text(error, + style: const TextStyle(color: Colors.red)) + : Container(), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.blue, + ), + onPressed: () => handleDeleteAction(bolus), + ), + ], + ), + ); + }, ), ); }, diff --git a/lib/screens/bolus/bolus_profile_detail.dart b/lib/screens/bolus/bolus_profile_detail.dart index 4a97cae..67dd802 100644 --- a/lib/screens/bolus/bolus_profile_detail.dart +++ b/lib/screens/bolus/bolus_profile_detail.dart @@ -227,7 +227,7 @@ class _BolusProfileDetailScreenState extends State { Widget build(BuildContext context) { bool isNew = widget.bolusProfile == null; return DefaultTabController( - length: 2, + length: isNew ? 1 : 2, child: Builder(builder: (BuildContext context) { final TabController tabController = DefaultTabController.of(context)!; tabController.addListener(() { @@ -235,6 +235,55 @@ class _BolusProfileDetailScreenState extends State { renderTabButtons(tabController.index); } }); + + List tabs = [ + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StyledForm( + formState: _bolusProfileForm, + fields: [ + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Name', + ), + validator: (value) { + if (value!.trim().isEmpty) { + return 'Empty title'; + } + return null; + }, + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'Notes', + alignLabelWithHint: true, + ), + controller: _notesController, + keyboardType: TextInputType.multiline, + ), + StyledBooleanFormField( + value: _active, + onChanged: (value) { + setState(() { + _active = value; + }); + }, + label: 'active', + ), + ], + ), + ], + ), + ), + ]; + + if (!isNew) { + tabs.add(BolusListScreen(bolusProfile: widget.bolusProfile)); + } + return Scaffold( appBar: AppBar( title: @@ -252,50 +301,7 @@ class _BolusProfileDetailScreenState extends State { drawer: const Navigation( currentLocation: BolusProfileDetailScreen.routeName), body: TabBarView( - children: [ - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StyledForm( - formState: _bolusProfileForm, - fields: [ - TextFormField( - controller: _nameController, - decoration: const InputDecoration( - labelText: 'Name', - ), - validator: (value) { - if (value!.trim().isEmpty) { - return 'Empty title'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Notes', - alignLabelWithHint: true, - ), - controller: _notesController, - keyboardType: TextInputType.multiline, - ), - StyledBooleanFormField( - value: _active, - onChanged: (value) { - setState(() { - _active = value; - }); - }, - label: 'active', - ), - ], - ), - ], - ), - ), - BolusListScreen(bolusProfile: widget.bolusProfile), - ], + children: tabs, ), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, diff --git a/lib/screens/log/log_entry.dart b/lib/screens/log/log_entry.dart index 7e86dbe..08dda05 100644 --- a/lib/screens/log/log_entry.dart +++ b/lib/screens/log/log_entry.dart @@ -217,7 +217,7 @@ class _LogEntryScreenState extends State { bool isNew = widget.entry == null; return DefaultTabController( - length: 3, + length: isNew ? 1 : 3, child: Builder(builder: (BuildContext context) { final TabController tabController = DefaultTabController.of(context)!; tabController.addListener(() { @@ -225,6 +225,16 @@ class _LogEntryScreenState extends State { renderTabButtons(tabController.index); } }); + List tabs = [ + LogEntryForm( + formState: logEntryForm, controllers: formDataControllers), + ]; + + if (!isNew) { + tabs.add(LogMealListScreen(logEntry: widget.entry)); + tabs.add(LogEventListScreen(logEntry: widget.entry)); + } + return Scaffold( appBar: AppBar( title: Text(isNew ? 'New Log Entry' : 'Edit Log Entry'), @@ -241,12 +251,7 @@ class _LogEntryScreenState extends State { ), drawer: const Navigation(currentLocation: LogEntryScreen.routeName), body: TabBarView( - children: [ - LogEntryForm( - formState: logEntryForm, controllers: formDataControllers), - LogMealListScreen(logEntry: widget.entry), - LogEventListScreen(logEntry: widget.entry), - ], + children: tabs, ), bottomNavigationBar: DetailBottomRow( onCancel: handleCancelAction, diff --git a/lib/screens/log/log_entry_form.dart b/lib/screens/log/log_entry_form.dart index 42a2bc9..86120c5 100644 --- a/lib/screens/log/log_entry_form.dart +++ b/lib/screens/log/log_entry_form.dart @@ -17,6 +17,35 @@ class LogEntryForm extends StatefulWidget { } class _LogEntryFormState extends State { + void convertBetweenMgPerDlAndMmolPerL({GlucoseMeasurement? calculateFrom}) { + int? mgPerDl; + double? mmolPerL; + final _mgPerDlController = widget.controllers['mgPerDl']; + final _mmolPerLController = widget.controllers['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) { final _timeController = widget.controllers['time']; @@ -48,8 +77,6 @@ class _LogEntryFormState extends State { // } //), Row( - // TODO: improve conversion of mg/dl and mmol/l - // TODO: display according to settings children: [ glucoseMeasurement == GlucoseMeasurement.mgPerDl || glucoseDisplayMode == GlucoseDisplayMode.both || @@ -61,6 +88,8 @@ class _LogEntryFormState extends State { suffixText: 'mg/dl', ), controller: _mgPerDlController, + onChanged: (_) => convertBetweenMgPerDlAndMmolPerL( + calculateFrom: GlucoseMeasurement.mgPerDl), keyboardType: const TextInputType.numberWithOptions(), validator: (value) { if (value!.trim().isEmpty && @@ -72,6 +101,14 @@ class _LogEntryFormState extends State { ), ) : 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 @@ -80,19 +117,10 @@ class _LogEntryFormState extends State { decoration: const InputDecoration( labelText: 'mmol/l', suffixText: 'mmol/l', - alignLabelWithHint: true, ), controller: _mmolPerLController, - onChanged: (_) { - setState(() { - _mgPerDlController!.text = - Utils.convertMmolPerLToMgPerDl( - double.tryParse( - _mgPerDlController.text) ?? - 0) - .toString(); - }); - }, + onChanged: (_) => convertBetweenMgPerDlAndMmolPerL( + calculateFrom: GlucoseMeasurement.mmolPerL), keyboardType: const TextInputType.numberWithOptions( decimal: true), validator: (value) { @@ -105,6 +133,14 @@ class _LogEntryFormState extends State { ), ) : Container(), + glucoseDisplayMode == GlucoseDisplayMode.both || + glucoseDisplayMode == GlucoseDisplayMode.bothForDetail + ? IconButton( + onPressed: () => convertBetweenMgPerDlAndMmolPerL( + calculateFrom: GlucoseMeasurement.mgPerDl), + icon: const Icon(Icons.calculate), + ) + : Container(), ], ), TextFormField( @@ -142,16 +178,6 @@ class _LogEntryFormState extends State { keyboardType: TextInputType.multiline, ), ], - // buttons: [ - // ElevatedButton( - // onPressed: handleCancelAction, - // child: const Text('CANCEL'), - // ), - // ElevatedButton( - // onPressed: handleSaveAction, - // child: const Text('SAVE'), - // ), - // ], ), ]), ); diff --git a/lib/utils/date_time_utils.dart b/lib/utils/date_time_utils.dart index 9023e30..b635c32 100644 --- a/lib/utils/date_time_utils.dart +++ b/lib/utils/date_time_utils.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class DateTimeUtils { + // TODO fix handling of time zones! + static String displayDateTime(DateTime? date, {String fallback = ''}) { if (date == null) { return fallback;