implement time period validation for bolus/basal profiles

This commit is contained in:
spinel 2021-10-26 01:11:58 +02:00
parent 68927522db
commit db023a94cf
9 changed files with 407 additions and 242 deletions

View File

@ -4,7 +4,6 @@ import 'package:diameter/config.dart';
import 'package:diameter/navigation.dart'; import 'package:diameter/navigation.dart';
import 'package:diameter/utils/date_time_utils.dart'; import 'package:diameter/utils/date_time_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:diameter/components/forms.dart'; import 'package:diameter/components/forms.dart';
import 'package:diameter/models/basal.dart'; import 'package:diameter/models/basal.dart';
import 'package:diameter/models/basal_profile.dart'; import 'package:diameter/models/basal_profile.dart';
@ -116,33 +115,40 @@ class _BasalDetailScreenState extends State<BasalDetailScreen> {
Row( Row(
children: [ children: [
Expanded( Expanded(
child: StyledTimeOfDayFormField( // TODO fix handling of time zones!
label: 'Start Time', child: Padding(
controller: _startTimeController, padding: const EdgeInsets.only(right: 5),
time: _startTime, child: StyledTimeOfDayFormField(
onChanged: (newStartTime) { label: 'Start Time',
if (newStartTime != null) { controller: _startTimeController,
setState(() { time: _startTime,
_startTime = newStartTime; onChanged: (newStartTime) {
}); if (newStartTime != null) {
updateStartTime(); setState(() {
} _startTime = newStartTime;
}, });
updateStartTime();
}
},
),
), ),
), ),
Expanded( Expanded(
child: StyledTimeOfDayFormField( child: Padding(
label: 'End Time', padding: const EdgeInsets.only(left: 5),
controller: _endTimeController, child: StyledTimeOfDayFormField(
time: _endTime, label: 'End Time',
onChanged: (newEndTime) { controller: _endTimeController,
if (newEndTime != null) { time: _endTime,
setState(() { onChanged: (newEndTime) {
_endTime = newEndTime; if (newEndTime != null) {
}); setState(() {
updateEndTime(); _endTime = newEndTime;
} });
}, updateEndTime();
}
},
),
), ),
), ),
], ],

View File

@ -1,5 +1,6 @@
import 'package:diameter/components/dialogs.dart'; import 'package:diameter/components/dialogs.dart';
import 'package:diameter/config.dart'; import 'package:diameter/config.dart';
import 'package:diameter/utils/date_time_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:diameter/components/progress_indicator.dart'; import 'package:diameter/components/progress_indicator.dart';
import 'package:diameter/models/basal.dart'; import 'package:diameter/models/basal.dart';
@ -64,6 +65,44 @@ class _BasalListScreenState extends State<BasalListScreen> {
} }
} }
String? checkBasalValidity(List<Basal> 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 @override
void initState() { void initState() {
super.initState(); super.initState();
@ -73,47 +112,60 @@ class _BasalListScreenState extends State<BasalListScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.only(top: 10.0),
child: Column( child: Column(
children: [ children: [
FutureBuilder<List<Basal>>( FutureBuilder<List<Basal>>(
future: widget.basalProfile!.basalRates, future: widget.basalProfile!.basalRates,
builder: (context, snapshot) { builder: (context, snapshot) {
return ViewWithProgressIndicator( return ViewWithProgressIndicator(
// TODO: add warning if time period is missing or has multiple rates
snapshot: snapshot, snapshot: snapshot,
child: snapshot.data == null || snapshot.data!.isEmpty child: snapshot.data == null || snapshot.data!.isEmpty
? const Padding( ? const Padding(
padding: EdgeInsets.all(10.0), padding: EdgeInsets.all(10.0),
child: Text('No Basal Rates for this Profile'), child: Text('No Basal Rates for this Profile'),
) )
: ListBody( : ListView.builder(
children: [ shrinkWrap: true,
DataTable( itemCount:
columnSpacing: 10.0, snapshot.data != null ? snapshot.data!.length : 0,
showCheckboxColumn: false, itemBuilder: (context, index) {
rows: snapshot.data != null final basal = snapshot.data![index];
? snapshot.data!.map((basal) { final error =
return DataRow( checkBasalValidity(snapshot.data!, index);
cells: basal.asDataTableCells([ return ListTile(
IconButton( tileColor:
icon: const Icon(Icons.edit), error != null ? Colors.red.shade100 : null,
iconSize: 16.0, onTap: () {
onPressed: () => handleEditAction(basal);
handleEditAction(basal)), },
IconButton( title: Row(
icon: const Icon(Icons.delete), mainAxisSize: MainAxisSize.max,
iconSize: 16.0, children: [
onPressed: () => Expanded(
handleDeleteAction(basal), child: Text(
), '${DateTimeUtils.displayTime(basal.startTime)} - ${DateTimeUtils.displayTime(basal.endTime)}')),
]), const Spacer(),
); Expanded(child: Text('${basal.units} U')),
}).toList() ],
: [], ),
columns: Basal.asDataTableColumns(), 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),
),
],
),
);
},
), ),
); );
}, },

View File

@ -227,7 +227,7 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool isNew = widget.basalProfile == null; bool isNew = widget.basalProfile == null;
return DefaultTabController( return DefaultTabController(
length: 2, length: isNew ? 1 : 2,
child: Builder(builder: (BuildContext context) { child: Builder(builder: (BuildContext context) {
final TabController tabController = DefaultTabController.of(context)!; final TabController tabController = DefaultTabController.of(context)!;
tabController.addListener(() { tabController.addListener(() {
@ -235,6 +235,55 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
renderTabButtons(tabController.index); renderTabButtons(tabController.index);
} }
}); });
List<Widget> 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: title:
@ -251,53 +300,7 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
), ),
drawer: const Navigation( drawer: const Navigation(
currentLocation: BasalProfileDetailScreen.routeName), currentLocation: BasalProfileDetailScreen.routeName),
body: TabBarView( body: TabBarView(children: tabs),
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),
],
),
bottomNavigationBar: DetailBottomRow( bottomNavigationBar: DetailBottomRow(
onCancel: handleCancelAction, onCancel: handleCancelAction,
onSave: handleSaveAction, onSave: handleSaveAction,

View File

@ -6,11 +6,9 @@ import 'package:diameter/settings.dart';
import 'package:diameter/utils/date_time_utils.dart'; import 'package:diameter/utils/date_time_utils.dart';
import 'package:diameter/utils/utils.dart'; import 'package:diameter/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:diameter/components/forms.dart'; import 'package:diameter/components/forms.dart';
import 'package:diameter/models/bolus.dart'; import 'package:diameter/models/bolus.dart';
import 'package:diameter/models/bolus_profile.dart'; import 'package:diameter/models/bolus_profile.dart';
import 'package:flutter/widgets.dart';
class BolusDetailScreen extends StatefulWidget { class BolusDetailScreen extends StatefulWidget {
static const String routeName = '/bolus'; static const String routeName = '/bolus';
@ -31,12 +29,12 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0); TimeOfDay _startTime = const TimeOfDay(hour: 0, minute: 0);
TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0); TimeOfDay _endTime = const TimeOfDay(hour: 0, minute: 0);
final _startTimeController = TextEditingController(); final _startTimeController = TextEditingController(text: '');
final _endTimeController = TextEditingController(); final _endTimeController = TextEditingController(text: '');
final _unitsController = TextEditingController(); final _unitsController = TextEditingController(text: '');
final _carbsController = TextEditingController(); final _carbsController = TextEditingController(text: '');
final _mgPerDlController = TextEditingController(); final _mgPerDlController = TextEditingController(text: '');
final _mmolPerLController = TextEditingController(); final _mmolPerLController = TextEditingController(text: '');
@override @override
void initState() { void initState() {
@ -136,7 +134,6 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
} }
void convertBetweenMgPerDlAndMmolPerL({GlucoseMeasurement? calculateFrom}) { void convertBetweenMgPerDlAndMmolPerL({GlucoseMeasurement? calculateFrom}) {
// TODO figure out why this isnt happening automatically
int? mgPerDl; int? mgPerDl;
double? mmolPerL; double? mmolPerL;
@ -182,33 +179,40 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
Row( Row(
children: [ children: [
Expanded( Expanded(
child: StyledTimeOfDayFormField( child: Padding(
label: 'Start Time', padding: const EdgeInsets.only(right: 5),
controller: _startTimeController, // TODO fix handling of time zones!
time: _startTime, child: StyledTimeOfDayFormField(
onChanged: (newStartTime) { label: 'Start Time',
if (newStartTime != null) { controller: _startTimeController,
setState(() { time: _startTime,
_startTime = newStartTime; onChanged: (newStartTime) {
}); if (newStartTime != null) {
updateStartTime(); setState(() {
} _startTime = newStartTime;
}, });
updateStartTime();
}
},
),
), ),
), ),
Expanded( Expanded(
child: StyledTimeOfDayFormField( child: Padding(
label: 'End Time', padding: const EdgeInsets.only(left: 5),
controller: _endTimeController, child: StyledTimeOfDayFormField(
time: _endTime, label: 'End Time',
onChanged: (newEndTime) { controller: _endTimeController,
if (newEndTime != null) { time: _endTime,
setState(() { onChanged: (newEndTime) {
_endTime = newEndTime; if (newEndTime != null) {
}); setState(() {
updateEndTime(); _endTime = newEndTime;
} });
}, updateEndTime();
}
},
),
), ),
), ),
], ],
@ -262,7 +266,9 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
), ),
controller: _mgPerDlController, controller: _mgPerDlController,
onChanged: (_) => onChanged: (_) =>
convertBetweenMgPerDlAndMmolPerL, convertBetweenMgPerDlAndMmolPerL(
calculateFrom:
GlucoseMeasurement.mgPerDl),
keyboardType: keyboardType:
const TextInputType.numberWithOptions(), const TextInputType.numberWithOptions(),
validator: (value) { validator: (value) {
@ -296,7 +302,9 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
), ),
controller: _mmolPerLController, controller: _mmolPerLController,
onChanged: (_) => onChanged: (_) =>
convertBetweenMgPerDlAndMmolPerL, convertBetweenMgPerDlAndMmolPerL(
calculateFrom:
GlucoseMeasurement.mmolPerL),
keyboardType: keyboardType:
const TextInputType.numberWithOptions( const TextInputType.numberWithOptions(
decimal: true), decimal: true),

View File

@ -1,5 +1,7 @@
import 'package:diameter/components/dialogs.dart'; import 'package:diameter/components/dialogs.dart';
import 'package:diameter/config.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:flutter/material.dart';
import 'package:diameter/components/progress_indicator.dart'; import 'package:diameter/components/progress_indicator.dart';
import 'package:diameter/models/bolus.dart'; import 'package:diameter/models/bolus.dart';
@ -64,6 +66,44 @@ class _BolusListScreenState extends State<BolusListScreen> {
} }
} }
String? checkBolusValidity(List<Bolus> 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 @override
void initState() { void initState() {
super.initState(); super.initState();
@ -80,41 +120,58 @@ class _BolusListScreenState extends State<BolusListScreen> {
future: widget.bolusProfile!.bolusRates, future: widget.bolusProfile!.bolusRates,
builder: (context, snapshot) { builder: (context, snapshot) {
return ViewWithProgressIndicator( return ViewWithProgressIndicator(
// TODO: add warning if time period is missing or has multiple rates
snapshot: snapshot, snapshot: snapshot,
child: snapshot.data == null || snapshot.data!.isEmpty child: snapshot.data == null || snapshot.data!.isEmpty
? const Padding( ? const Padding(
padding: EdgeInsets.all(10.0), padding: EdgeInsets.all(10.0),
child: Text('No Bolus Rates for this Profile'), child: Text('No Basal Rates for this Profile'),
) )
: ListBody( : ListView.builder(
children: [ shrinkWrap: true,
DataTable( itemCount:
columnSpacing: 10.0, snapshot.data != null ? snapshot.data!.length : 0,
showCheckboxColumn: false, itemBuilder: (context, index) {
rows: snapshot.data != null final bolus = snapshot.data![index];
? snapshot.data!.map((bolus) { final error =
return DataRow( checkBolusValidity(snapshot.data!, index);
cells: bolus.asDataTableCells( return ListTile(
[ tileColor:
IconButton( error != null ? Colors.red.shade100 : null,
icon: const Icon(Icons.edit), onTap: () {
iconSize: 16.0, handleEditAction(bolus);
onPressed: () => },
handleEditAction(bolus)), title: Row(
IconButton( mainAxisSize: MainAxisSize.max,
icon: const Icon(Icons.delete), children: [
iconSize: 16.0, Expanded(
onPressed: () => child: Text(
handleDeleteAction(bolus)), '${DateTimeUtils.displayTime(bolus.startTime)} - ${DateTimeUtils.displayTime(bolus.endTime)}')),
], // TODO: style this
), Expanded(
); child: Text(
}).toList() '${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)),
columns: Bolus.asDataTableColumns(), ),
), ],
], ),
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),
),
],
),
);
},
), ),
); );
}, },

View File

@ -227,7 +227,7 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool isNew = widget.bolusProfile == null; bool isNew = widget.bolusProfile == null;
return DefaultTabController( return DefaultTabController(
length: 2, length: isNew ? 1 : 2,
child: Builder(builder: (BuildContext context) { child: Builder(builder: (BuildContext context) {
final TabController tabController = DefaultTabController.of(context)!; final TabController tabController = DefaultTabController.of(context)!;
tabController.addListener(() { tabController.addListener(() {
@ -235,6 +235,55 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
renderTabButtons(tabController.index); renderTabButtons(tabController.index);
} }
}); });
List<Widget> 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: title:
@ -252,50 +301,7 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
drawer: const Navigation( drawer: const Navigation(
currentLocation: BolusProfileDetailScreen.routeName), currentLocation: BolusProfileDetailScreen.routeName),
body: TabBarView( body: TabBarView(
children: [ children: 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',
),
],
),
],
),
),
BolusListScreen(bolusProfile: widget.bolusProfile),
],
), ),
bottomNavigationBar: DetailBottomRow( bottomNavigationBar: DetailBottomRow(
onCancel: handleCancelAction, onCancel: handleCancelAction,

View File

@ -217,7 +217,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
bool isNew = widget.entry == null; bool isNew = widget.entry == null;
return DefaultTabController( return DefaultTabController(
length: 3, length: isNew ? 1 : 3,
child: Builder(builder: (BuildContext context) { child: Builder(builder: (BuildContext context) {
final TabController tabController = DefaultTabController.of(context)!; final TabController tabController = DefaultTabController.of(context)!;
tabController.addListener(() { tabController.addListener(() {
@ -225,6 +225,16 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
renderTabButtons(tabController.index); renderTabButtons(tabController.index);
} }
}); });
List<Widget> tabs = [
LogEntryForm(
formState: logEntryForm, controllers: formDataControllers),
];
if (!isNew) {
tabs.add(LogMealListScreen(logEntry: widget.entry));
tabs.add(LogEventListScreen(logEntry: widget.entry));
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(isNew ? 'New Log Entry' : 'Edit Log Entry'), title: Text(isNew ? 'New Log Entry' : 'Edit Log Entry'),
@ -241,12 +251,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
), ),
drawer: const Navigation(currentLocation: LogEntryScreen.routeName), drawer: const Navigation(currentLocation: LogEntryScreen.routeName),
body: TabBarView( body: TabBarView(
children: [ children: tabs,
LogEntryForm(
formState: logEntryForm, controllers: formDataControllers),
LogMealListScreen(logEntry: widget.entry),
LogEventListScreen(logEntry: widget.entry),
],
), ),
bottomNavigationBar: DetailBottomRow( bottomNavigationBar: DetailBottomRow(
onCancel: handleCancelAction, onCancel: handleCancelAction,

View File

@ -17,6 +17,35 @@ class LogEntryForm extends StatefulWidget {
} }
class _LogEntryFormState extends State<LogEntryForm> { class _LogEntryFormState extends State<LogEntryForm> {
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final _timeController = widget.controllers['time']; final _timeController = widget.controllers['time'];
@ -48,8 +77,6 @@ class _LogEntryFormState extends State<LogEntryForm> {
// } // }
//), //),
Row( Row(
// TODO: improve conversion of mg/dl and mmol/l
// TODO: display according to settings
children: [ children: [
glucoseMeasurement == GlucoseMeasurement.mgPerDl || glucoseMeasurement == GlucoseMeasurement.mgPerDl ||
glucoseDisplayMode == GlucoseDisplayMode.both || glucoseDisplayMode == GlucoseDisplayMode.both ||
@ -61,6 +88,8 @@ class _LogEntryFormState extends State<LogEntryForm> {
suffixText: 'mg/dl', suffixText: 'mg/dl',
), ),
controller: _mgPerDlController, controller: _mgPerDlController,
onChanged: (_) => convertBetweenMgPerDlAndMmolPerL(
calculateFrom: GlucoseMeasurement.mgPerDl),
keyboardType: const TextInputType.numberWithOptions(), keyboardType: const TextInputType.numberWithOptions(),
validator: (value) { validator: (value) {
if (value!.trim().isEmpty && if (value!.trim().isEmpty &&
@ -72,6 +101,14 @@ class _LogEntryFormState extends State<LogEntryForm> {
), ),
) )
: Container(), : Container(),
glucoseDisplayMode == GlucoseDisplayMode.both ||
glucoseDisplayMode == GlucoseDisplayMode.bothForDetail
? IconButton(
onPressed: () => convertBetweenMgPerDlAndMmolPerL(
calculateFrom: GlucoseMeasurement.mmolPerL),
icon: const Icon(Icons.calculate),
)
: Container(),
glucoseMeasurement == GlucoseMeasurement.mmolPerL || glucoseMeasurement == GlucoseMeasurement.mmolPerL ||
glucoseDisplayMode == GlucoseDisplayMode.both || glucoseDisplayMode == GlucoseDisplayMode.both ||
glucoseDisplayMode == GlucoseDisplayMode.bothForDetail glucoseDisplayMode == GlucoseDisplayMode.bothForDetail
@ -80,19 +117,10 @@ class _LogEntryFormState extends State<LogEntryForm> {
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'mmol/l', labelText: 'mmol/l',
suffixText: 'mmol/l', suffixText: 'mmol/l',
alignLabelWithHint: true,
), ),
controller: _mmolPerLController, controller: _mmolPerLController,
onChanged: (_) { onChanged: (_) => convertBetweenMgPerDlAndMmolPerL(
setState(() { calculateFrom: GlucoseMeasurement.mmolPerL),
_mgPerDlController!.text =
Utils.convertMmolPerLToMgPerDl(
double.tryParse(
_mgPerDlController.text) ??
0)
.toString();
});
},
keyboardType: const TextInputType.numberWithOptions( keyboardType: const TextInputType.numberWithOptions(
decimal: true), decimal: true),
validator: (value) { validator: (value) {
@ -105,6 +133,14 @@ class _LogEntryFormState extends State<LogEntryForm> {
), ),
) )
: Container(), : Container(),
glucoseDisplayMode == GlucoseDisplayMode.both ||
glucoseDisplayMode == GlucoseDisplayMode.bothForDetail
? IconButton(
onPressed: () => convertBetweenMgPerDlAndMmolPerL(
calculateFrom: GlucoseMeasurement.mgPerDl),
icon: const Icon(Icons.calculate),
)
: Container(),
], ],
), ),
TextFormField( TextFormField(
@ -142,16 +178,6 @@ class _LogEntryFormState extends State<LogEntryForm> {
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
), ),
], ],
// buttons: [
// ElevatedButton(
// onPressed: handleCancelAction,
// child: const Text('CANCEL'),
// ),
// ElevatedButton(
// onPressed: handleSaveAction,
// child: const Text('SAVE'),
// ),
// ],
), ),
]), ]),
); );

View File

@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class DateTimeUtils { class DateTimeUtils {
// TODO fix handling of time zones!
static String displayDateTime(DateTime? date, {String fallback = ''}) { static String displayDateTime(DateTime? date, {String fallback = ''}) {
if (date == null) { if (date == null) {
return fallback; return fallback;