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/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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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'),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user