implement time period validation for bolus/basal profiles
This commit is contained in:
parent
68927522db
commit
db023a94cf
@ -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<BasalDetailScreen> {
|
||||
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();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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<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
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -73,47 +112,60 @@ class _BasalListScreenState extends State<BasalListScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(top: 10.0),
|
||||
child: Column(
|
||||
children: [
|
||||
FutureBuilder<List<Basal>>(
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -227,7 +227,7 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
|
||||
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<BasalProfileDetailScreen> {
|
||||
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(
|
||||
appBar: AppBar(
|
||||
title:
|
||||
@ -251,53 +300,7 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
|
||||
),
|
||||
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,
|
||||
|
@ -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<BolusDetailScreen> {
|
||||
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<BolusDetailScreen> {
|
||||
}
|
||||
|
||||
void convertBetweenMgPerDlAndMmolPerL({GlucoseMeasurement? calculateFrom}) {
|
||||
// TODO figure out why this isnt happening automatically
|
||||
int? mgPerDl;
|
||||
double? mmolPerL;
|
||||
|
||||
@ -182,33 +179,40 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
|
||||
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<BolusDetailScreen> {
|
||||
),
|
||||
controller: _mgPerDlController,
|
||||
onChanged: (_) =>
|
||||
convertBetweenMgPerDlAndMmolPerL,
|
||||
convertBetweenMgPerDlAndMmolPerL(
|
||||
calculateFrom:
|
||||
GlucoseMeasurement.mgPerDl),
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(),
|
||||
validator: (value) {
|
||||
@ -296,7 +302,9 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
|
||||
),
|
||||
controller: _mmolPerLController,
|
||||
onChanged: (_) =>
|
||||
convertBetweenMgPerDlAndMmolPerL,
|
||||
convertBetweenMgPerDlAndMmolPerL(
|
||||
calculateFrom:
|
||||
GlucoseMeasurement.mmolPerL),
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(
|
||||
decimal: true),
|
||||
|
@ -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<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
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -80,41 +120,58 @@ class _BolusListScreenState extends State<BolusListScreen> {
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -227,7 +227,7 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
|
||||
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<BolusProfileDetailScreen> {
|
||||
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(
|
||||
appBar: AppBar(
|
||||
title:
|
||||
@ -252,50 +301,7 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
|
||||
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,
|
||||
|
@ -217,7 +217,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
|
||||
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<LogEntryScreen> {
|
||||
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(
|
||||
appBar: AppBar(
|
||||
title: Text(isNew ? 'New Log Entry' : 'Edit Log Entry'),
|
||||
@ -241,12 +251,7 @@ class _LogEntryScreenState extends State<LogEntryScreen> {
|
||||
),
|
||||
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,
|
||||
|
@ -17,6 +17,35 @@ class LogEntryForm extends StatefulWidget {
|
||||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final _timeController = widget.controllers['time'];
|
||||
@ -48,8 +77,6 @@ class _LogEntryFormState extends State<LogEntryForm> {
|
||||
// }
|
||||
//),
|
||||
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<LogEntryForm> {
|
||||
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<LogEntryForm> {
|
||||
),
|
||||
)
|
||||
: 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<LogEntryForm> {
|
||||
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<LogEntryForm> {
|
||||
),
|
||||
)
|
||||
: 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<LogEntryForm> {
|
||||
keyboardType: TextInputType.multiline,
|
||||
),
|
||||
],
|
||||
// buttons: [
|
||||
// ElevatedButton(
|
||||
// onPressed: handleCancelAction,
|
||||
// child: const Text('CANCEL'),
|
||||
// ),
|
||||
// ElevatedButton(
|
||||
// onPressed: handleSaveAction,
|
||||
// child: const Text('SAVE'),
|
||||
// ),
|
||||
// ],
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user