validate time periods for bolus and basal and make auto suggestions

This commit is contained in:
spinel 2021-10-27 01:25:40 +02:00
parent db023a94cf
commit 53fb4ac7fc
13 changed files with 555 additions and 353 deletions

View File

@ -1,5 +1,5 @@
import 'package:diameter/utils/date_time_utils.dart';
import 'package:flutter/material.dart';
// import 'package:diameter/utils/date_time_utils.dart';
// import 'package:flutter/material.dart';
import 'package:parse_server_sdk_flutter/parse_server_sdk.dart';
import 'package:diameter/models/basal_profile.dart';
import 'package:diameter/components/data_table.dart';
@ -14,8 +14,8 @@ class Basal extends DataTableContent {
Basal(ParseObject? object) {
if (object != null) {
objectId = object.get<String>('objectId');
startTime = object.get<DateTime>('startTime')!;
endTime = object.get<DateTime>('endTime')!;
startTime = object.get<DateTime>('startTime')!.toLocal();
endTime = object.get<DateTime>('endTime')!.toLocal();
units = object.get<num>('units')! / 100;
basalProfile =
object.get<ParseObject>('basalProfile')!.get<String>('objectId')!;
@ -40,7 +40,8 @@ class Basal extends DataTableContent {
..whereEqualTo(
'basalProfile',
(ParseObject('BasalProfile')..objectId = basalProfile.objectId!)
.toPointer());
.toPointer())
..orderByAscending('startTime');
final ParseResponse apiResponse = await query.query();
if (apiResponse.success && apiResponse.results != null) {
@ -57,8 +58,8 @@ class Basal extends DataTableContent {
required String basalProfile,
}) async {
final basal = ParseObject('Basal')
..set('startTime', startTime)
..set('endTime', endTime)
..set('startTime', startTime.toUtc())
..set('endTime', endTime.toUtc())
..set('units', units * 100)
..set('basalProfile',
(ParseObject('BasalProfile')..objectId = basalProfile).toPointer());
@ -73,10 +74,10 @@ class Basal extends DataTableContent {
}) async {
var basal = ParseObject('Basal')..objectId = objectId;
if (startTime != null) {
basal.set('startTime', startTime);
basal.set('startTime', startTime.toUtc());
}
if (endTime != null) {
basal.set('endTime', endTime);
basal.set('endTime', endTime.toUtc());
}
if (units != null) {
basal.set('units', units * 100);
@ -88,27 +89,27 @@ class Basal extends DataTableContent {
var basal = ParseObject('Basal')..objectId = objectId;
await basal.delete();
}
@override
List<DataCell> asDataTableCells(List<Widget>? actions) {
return [
DataCell(Text(DateTimeUtils.displayTime(startTime))),
DataCell(Text(DateTimeUtils.displayTime(endTime))),
DataCell(Text('${units.toString()} U')),
DataCell(
Row(
children: actions ?? [],
),
),
];
}
static List<DataColumn> asDataTableColumns() {
return [
const DataColumn(label: Expanded(child: Text('Start Time'))),
const DataColumn(label: Expanded(child: Text('End Time'))),
const DataColumn(label: Expanded(child: Text('Units'))),
const DataColumn(label: Expanded(child: Text('Actions'))),
];
}
//
// @override
// List<DataCell> asDataTableCells(List<Widget>? actions) {
// return [
// DataCell(Text(DateTimeUtils.displayTime(startTime))),
// DataCell(Text(DateTimeUtils.displayTime(endTime))),
// DataCell(Text('${units.toString()} U')),
// DataCell(
// Row(
// children: actions ?? [],
// ),
// ),
// ];
// }
//
// static List<DataColumn> asDataTableColumns() {
// return [
// const DataColumn(label: Expanded(child: Text('Start Time'))),
// const DataColumn(label: Expanded(child: Text('End Time'))),
// const DataColumn(label: Expanded(child: Text('Units'))),
// const DataColumn(label: Expanded(child: Text('Actions'))),
// ];
// }
}

View File

@ -1,8 +1,8 @@
import 'package:diameter/config.dart';
import 'package:diameter/settings.dart';
import 'package:diameter/utils/date_time_utils.dart';
// import 'package:diameter/config.dart';
// import 'package:diameter/settings.dart';
// import 'package:diameter/utils/date_time_utils.dart';
import 'package:diameter/utils/utils.dart';
import 'package:flutter/material.dart';
// import 'package:flutter/material.dart';
import 'package:parse_server_sdk_flutter/parse_server_sdk.dart';
import 'package:diameter/components/data_table.dart';
import 'package:diameter/models/bolus_profile.dart';
@ -20,8 +20,8 @@ class Bolus extends DataTableContent {
Bolus(ParseObject? object) {
if (object != null) {
objectId = object.get<String>('objectId');
startTime = object.get<DateTime>('startTime')!;
endTime = object.get<DateTime>('endTime')!;
startTime = object.get<DateTime>('startTime')!.toLocal();
endTime = object.get<DateTime>('endTime')!.toLocal();
units = object.get<num>('units')! / 100;
carbs = object.get<num>('carbs')!.toDouble();
mgPerDl = object.get<num>('mgPerDl') != null
@ -39,6 +39,7 @@ class Bolus extends DataTableContent {
QueryBuilder<ParseObject> query =
QueryBuilder<ParseObject>(ParseObject('Bolus'))
..whereEqualTo('objectId', objectId);
final ParseResponse apiResponse = await query.query();
if (apiResponse.success && apiResponse.results != null) {
@ -53,7 +54,8 @@ class Bolus extends DataTableContent {
..whereEqualTo(
'bolusProfile',
(ParseObject('BolusProfile')..objectId = bolusProfile.objectId!)
.toPointer());
.toPointer())
..orderByAscending('startTime');
final ParseResponse apiResponse = await query.query();
if (apiResponse.success && apiResponse.results != null) {
@ -73,8 +75,8 @@ class Bolus extends DataTableContent {
required String bolusProfile,
}) async {
final bolus = ParseObject('Bolus')
..set('startTime', startTime)
..set('endTime', endTime)
..set('startTime', startTime.toUtc())
..set('endTime', endTime.toUtc())
..set('units', units * 100)
..set('carbs', carbs.round())
..set('bolusProfile',
@ -105,10 +107,10 @@ class Bolus extends DataTableContent {
}) async {
var bolus = ParseObject('Bolus')..objectId = objectId;
if (startTime != null) {
bolus.set('startTime', startTime);
bolus.set('startTime', startTime.toUtc());
}
if (endTime != null) {
bolus.set('endTime', endTime);
bolus.set('endTime', endTime.toUtc());
}
if (units != null) {
bolus.set('units', units * 100);
@ -137,63 +139,63 @@ class Bolus extends DataTableContent {
var bolus = ParseObject('Bolus')..objectId = objectId;
await bolus.delete();
}
@override
List<DataCell> asDataTableCells(List<Widget>? actions) {
var cols = [
DataCell(Text(DateTimeUtils.displayTime(startTime))),
DataCell(Text(DateTimeUtils.displayTime(endTime))),
DataCell(Text('${units.toString()} U')),
DataCell(Text('${carbs.toString()} g')),
];
if (glucoseMeasurement == GlucoseMeasurement.mgPerDl ||
glucoseDisplayMode == GlucoseDisplayMode.both ||
glucoseDisplayMode == GlucoseDisplayMode.bothForList) {
cols.add(DataCell(Text('${mgPerDl.toString()} mg/dl')));
}
if (glucoseMeasurement == GlucoseMeasurement.mmolPerL ||
glucoseDisplayMode == GlucoseDisplayMode.both ||
glucoseDisplayMode == GlucoseDisplayMode.bothForList) {
cols.add(DataCell(Text('${mmolPerL.toString()} mmol/l')));
}
cols.add(
DataCell(
Row(
children: actions ?? [],
),
),
);
return cols;
}
static List<DataColumn> asDataTableColumns() {
var cols = [
const DataColumn(label: Expanded(child: Text('Start Time'))),
const DataColumn(label: Expanded(child: Text('End Time'))),
const DataColumn(label: Expanded(child: Text('Units'))),
const DataColumn(label: Expanded(child: Text('per Carbs'))),
];
if (glucoseMeasurement == GlucoseMeasurement.mgPerDl ||
glucoseDisplayMode == GlucoseDisplayMode.both ||
glucoseDisplayMode == GlucoseDisplayMode.bothForList) {
cols.add(const DataColumn(label: Expanded(child: Text('per mg/dl'))));
}
if (glucoseMeasurement == GlucoseMeasurement.mmolPerL ||
glucoseDisplayMode == GlucoseDisplayMode.both ||
glucoseDisplayMode == GlucoseDisplayMode.bothForList) {
cols.add(const DataColumn(label: Expanded(child: Text('per mmol/l'))));
}
cols.add(
const DataColumn(label: Expanded(child: Text('Actions'))),
);
return cols;
}
//
// @override
// List<DataCell> asDataTableCells(List<Widget>? actions) {
// var cols = [
// DataCell(Text(DateTimeUtils.displayTime(startTime))),
// DataCell(Text(DateTimeUtils.displayTime(endTime))),
// DataCell(Text('${units.toString()} U')),
// DataCell(Text('${carbs.toString()} g')),
// ];
//
// if (glucoseMeasurement == GlucoseMeasurement.mgPerDl ||
// glucoseDisplayMode == GlucoseDisplayMode.both ||
// glucoseDisplayMode == GlucoseDisplayMode.bothForList) {
// cols.add(DataCell(Text('${mgPerDl.toString()} mg/dl')));
// }
//
// if (glucoseMeasurement == GlucoseMeasurement.mmolPerL ||
// glucoseDisplayMode == GlucoseDisplayMode.both ||
// glucoseDisplayMode == GlucoseDisplayMode.bothForList) {
// cols.add(DataCell(Text('${mmolPerL.toString()} mmol/l')));
// }
//
// cols.add(
// DataCell(
// Row(
// children: actions ?? [],
// ),
// ),
// );
//
// return cols;
// }
//
// static List<DataColumn> asDataTableColumns() {
// var cols = [
// const DataColumn(label: Expanded(child: Text('Start Time'))),
// const DataColumn(label: Expanded(child: Text('End Time'))),
// const DataColumn(label: Expanded(child: Text('Units'))),
// const DataColumn(label: Expanded(child: Text('per Carbs'))),
// ];
//
// if (glucoseMeasurement == GlucoseMeasurement.mgPerDl ||
// glucoseDisplayMode == GlucoseDisplayMode.both ||
// glucoseDisplayMode == GlucoseDisplayMode.bothForList) {
// cols.add(const DataColumn(label: Expanded(child: Text('per mg/dl'))));
// }
//
// if (glucoseMeasurement == GlucoseMeasurement.mmolPerL ||
// glucoseDisplayMode == GlucoseDisplayMode.both ||
// glucoseDisplayMode == GlucoseDisplayMode.bothForList) {
// cols.add(const DataColumn(label: Expanded(child: Text('per mmol/l'))));
// }
//
// cols.add(
// const DataColumn(label: Expanded(child: Text('Actions'))),
// );
//
// return cols;
// }
}

View File

@ -20,7 +20,7 @@ class LogEntry {
LogEntry(ParseObject object) {
objectId = object.get<String>('objectId');
time = object.get<DateTime>('time')!;
time = object.get<DateTime>('time')!.toLocal();
mgPerDl = object.get<num>('mgPerDl') != null
? object.get<num>('mgPerDl')!.toInt()
: null;
@ -58,8 +58,8 @@ class LogEntry {
static Future<List<LogEntry>> fetchAllForRange(DateTimeRange range) async {
QueryBuilder<ParseObject> query =
QueryBuilder<ParseObject>(ParseObject('LogEntry'))
..whereGreaterThanOrEqualsTo('time', range.start)
..whereLessThanOrEqualTo('time', range.end)
..whereGreaterThanOrEqualsTo('time', range.start.toUtc())
..whereLessThanOrEqualTo('time', range.end.toUtc())
..orderByAscending('time');
final ParseResponse apiResponse = await query.query();
@ -110,7 +110,7 @@ class LogEntry {
String? notes,
}) async {
final logEntry = ParseObject('LogEntry')
..set('time', time)
..set('time', time.toUtc())
..set('bolusGlucose', bolusGlucose)
..set('delayedBolusDuration', delayedBolusDuration)
..set('delayedBolusRatio', delayedBolusRatio)
@ -145,7 +145,7 @@ class LogEntry {
final logEntry = ParseObject('LogEntry');
if (time != null) {
logEntry.set('time', time);
logEntry.set('time', time.toUtc());
}
if (bolusGlucose != null) {

View File

@ -23,8 +23,8 @@ class LogEvent extends DataTableContent {
object.get<ParseObject>('endLogEntry')?.get<String>('objectId');
eventType =
object.get<ParseObject>('eventType')!.get<String>('objectId')!;
time = object.get<DateTime>('time')!;
endTime = object.get<DateTime>('endTime');
time = object.get<DateTime>('time')!.toLocal();
endTime = object.get<DateTime>('endTime')?.toLocal();
hasEndTime = object.get<bool>('hasEndTime')!;
notes = object.get<String>('notes');
}
@ -101,7 +101,7 @@ class LogEvent extends DataTableContent {
(ParseObject('LogEntry')..objectId = logEntry).toPointer())
..set('eventType',
(ParseObject('LogEventType')..objectId = eventType).toPointer())
..set('time', time)
..set('time', time.toUtc())
..set('hasEndTime', hasEndTime)
..set('notes', notes);
await logEvent.save();
@ -126,10 +126,10 @@ class LogEvent extends DataTableContent {
(ParseObject('LogEntry')..objectId = endLogEntry).toPointer());
}
if (time != null) {
logEvent.set('time', time);
logEvent.set('time', time.toUtc());
}
if (endTime != null) {
logEvent.set('endTime', endTime);
logEvent.set('endTime', endTime.toUtc());
}
if (hasEndTime != null) {
logEvent.set('hasEndTime', hasEndTime);
@ -144,33 +144,33 @@ class LogEvent extends DataTableContent {
var logEvent = ParseObject('LogEvent')..objectId = objectId;
await logEvent.delete();
}
@override
List<DataCell> asDataTableCells(List<Widget> actions,
{List<LogEventType>? types}) {
return [
DataCell(Text(
types?.firstWhere((element) => element.objectId == eventType).value ??
types?.length.toString() ??
'')),
DataCell(Text(DateTimeUtils.displayDateTime(time))),
DataCell(Text(hasEndTime
? DateTimeUtils.displayDateTime(endTime, fallback: 'ongoing')
: '-')),
DataCell(
Row(
children: actions,
),
),
];
}
static List<DataColumn> asDataTableColumns() {
return [
const DataColumn(label: Expanded(child: Text('Event Type'))),
const DataColumn(label: Expanded(child: Text('Start Time'))),
const DataColumn(label: Expanded(child: Text('End Time'))),
const DataColumn(label: Expanded(child: Text('Actions'))),
];
}
//
// @override
// List<DataCell> asDataTableCells(List<Widget> actions,
// {List<LogEventType>? types}) {
// return [
// DataCell(Text(
// types?.firstWhere((element) => element.objectId == eventType).value ??
// types?.length.toString() ??
// '')),
// DataCell(Text(DateTimeUtils.displayDateTime(time))),
// DataCell(Text(hasEndTime
// ? DateTimeUtils.displayDateTime(endTime, fallback: 'ongoing')
// : '-')),
// DataCell(
// Row(
// children: actions,
// ),
// ),
// ];
// }
//
// static List<DataColumn> asDataTableColumns() {
// return [
// const DataColumn(label: Expanded(child: Text('Event Type'))),
// const DataColumn(label: Expanded(child: Text('Start Time'))),
// const DataColumn(label: Expanded(child: Text('End Time'))),
// const DataColumn(label: Expanded(child: Text('Actions'))),
// ];
// }
}

View File

@ -13,8 +13,15 @@ class BasalDetailScreen extends StatefulWidget {
final BasalProfile basalProfile;
final Basal? basal;
final TimeOfDay? suggestedStartTime;
final TimeOfDay? suggestedEndTime;
const BasalDetailScreen({Key? key, required this.basalProfile, this.basal})
const BasalDetailScreen(
{Key? key,
required this.basalProfile,
this.basal,
this.suggestedStartTime,
this.suggestedEndTime})
: super(key: key);
@override
@ -33,6 +40,12 @@ class _BasalDetailScreenState extends State<BasalDetailScreen> {
@override
void initState() {
super.initState();
if (widget.suggestedStartTime != null) {
_startTime = widget.suggestedStartTime!;
}
if (widget.suggestedEndTime != null) {
_endTime = widget.suggestedEndTime!;
}
if (widget.basal != null) {
_startTime = TimeOfDay.fromDateTime(widget.basal!.startTime);
_endTime = TimeOfDay.fromDateTime(widget.basal!.endTime);
@ -51,35 +64,90 @@ class _BasalDetailScreenState extends State<BasalDetailScreen> {
_endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime);
}
Future<String?> validateTimePeriod() async {
String? error;
List<Basal> basalRates =
await Basal.fetchAllForBasalProfile(widget.basalProfile);
// check for duplicates
if (basalRates
.where((other) =>
(widget.basal == null ||
widget.basal!.objectId != other.objectId) &&
_startTime.hour == other.startTime.hour &&
_startTime.minute == other.startTime.minute)
.isNotEmpty) {
error = 'There\'s already a rate with this start time.';
}
if (basalRates
.where((other) =>
(widget.basal == null ||
widget.basal!.objectId != other.objectId) &&
DateTimeUtils.convertTimeOfDayToDateTime(_startTime)
.isBefore(other.startTime) &&
DateTimeUtils.convertTimeOfDayToDateTime(_endTime)
.isAfter(other.startTime))
.isNotEmpty) {
error = 'This rate\'s time period overlaps with another one.';
}
return error == null
? null
: showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text(error!),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'CANCEL'),
child: const Text('GO BACK TO EDITING'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, 'CONFIRM'),
child: const Text('SAVE AS IS'),
),
],
);
});
}
void handleSaveAction() async {
// TODO: add confirmation dialog in case time period is already covered
if (_basalForm.currentState!.validate()) {
await validateTimePeriod().then((value) async {
if (value != 'CANCEL') {
bool isNew = widget.basal == null;
isNew
? await Basal.save(
startTime: DateTimeUtils.convertTimeOfDayToDateTime(_startTime),
startTime:
DateTimeUtils.convertTimeOfDayToDateTime(_startTime),
endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime),
units: double.parse(_unitsController.text),
basalProfile: widget.basalProfile.objectId!,
)
: await Basal.update(
widget.basal!.objectId!,
startTime: DateTimeUtils.convertTimeOfDayToDateTime(_startTime),
startTime:
DateTimeUtils.convertTimeOfDayToDateTime(_startTime),
endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime),
units: double.parse(_unitsController.text),
);
Navigator.pop(context, '${isNew ? 'New' : ''} Basal Rate saved');
}
});
}
}
void handleCancelAction() {
bool isNew = widget.basal == null;
if (showConfirmationDialogOnCancel &&
((isNew &&
(_startTime.hour != 0 ||
_endTime.hour != 0 ||
_startTime.minute != 0 ||
_endTime.minute != 0 ||
(_startTime.hour != (widget.suggestedStartTime?.hour ?? 0) ||
_endTime.hour != (widget.suggestedEndTime?.hour ?? 0) ||
_startTime.minute !=
(widget.suggestedStartTime?.minute ?? 0) ||
_endTime.minute != (widget.suggestedEndTime?.minute ?? 0) ||
double.tryParse(_unitsController.text) != null)) ||
(!isNew &&
(TimeOfDay.fromDateTime(widget.basal!.startTime) !=
@ -115,7 +183,6 @@ class _BasalDetailScreenState extends State<BasalDetailScreen> {
Row(
children: [
Expanded(
// TODO fix handling of time zones!
child: Padding(
padding: const EdgeInsets.only(right: 5),
child: StyledTimeOfDayFormField(

View File

@ -65,12 +65,12 @@ class _BasalListScreenState extends State<BasalListScreen> {
}
}
String? checkBasalValidity(List<Basal> basalRates, int index) {
String? validateTimePeriod(List<Basal> basalRates, int index) {
Basal basal = basalRates[index];
// check for gaps
if (index == 0 &&
(basal.startTime.toLocal().hour != 0 || basal.startTime.minute != 0)) {
(basal.startTime.hour != 0 || basal.startTime.minute != 0)) {
return 'First Basal of the day needs to start at 00:00';
}
@ -82,12 +82,11 @@ class _BasalListScreenState extends State<BasalListScreen> {
}
if (index == basalRates.length - 1 &&
(basal.endTime.toLocal().hour != 0 || basal.endTime.minute != 0)) {
(basal.endTime.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) {
@ -131,7 +130,7 @@ class _BasalListScreenState extends State<BasalListScreen> {
itemBuilder: (context, index) {
final basal = snapshot.data![index];
final error =
checkBasalValidity(snapshot.data!, index);
validateTimePeriod(snapshot.data!, index);
return ListTile(
tileColor:
error != null ? Colors.red.shade100 : null,

View File

@ -48,16 +48,7 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
}
addBasalButton = FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return BasalDetailScreen(basalProfile: widget.basalProfile!);
},
),
).then((message) => refresh(message: message));
},
onPressed: handleAddNew,
child: const Icon(Icons.add),
);
@ -163,6 +154,46 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
}
}
void handleAddNew() async {
List<Basal> basalRates =
await Basal.fetchAllForBasalProfile(widget.basalProfile!);
TimeOfDay? suggestedStartTime;
TimeOfDay? suggestedEndTime;
basalRates.asMap().forEach((index, basal) {
if (suggestedStartTime == null && suggestedEndTime == null) {
if (index == 0 &&
(basal.startTime.hour != 0 || basal.startTime.minute != 0)) {
suggestedStartTime = const TimeOfDay(hour: 0, minute: 0);
suggestedEndTime = TimeOfDay.fromDateTime(basal.startTime);
} else if ((index == basalRates.length - 1) &&
(basal.endTime.hour != 0 || basal.endTime.minute != 0)) {
suggestedStartTime = TimeOfDay.fromDateTime(basal.endTime);
suggestedEndTime = const TimeOfDay(hour: 0, minute: 0);
} else if (index != 0) {
var lastEndTime = basalRates[index - 1].endTime;
if (basal.startTime.isAfter(lastEndTime)) {
suggestedStartTime = TimeOfDay.fromDateTime(lastEndTime);
suggestedEndTime = TimeOfDay.fromDateTime(basal.startTime);
}
}
}
});
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return BasalDetailScreen(
basalProfile: widget.basalProfile!,
suggestedStartTime: suggestedStartTime,
suggestedEndTime: suggestedEndTime,
);
},
),
).then((message) => refresh(message: message));
}
void handleSaveAction() async {
if (_basalProfileForm.currentState!.validate()) {
await checkActiveProfiles();
@ -187,7 +218,7 @@ class _BasalProfileDetailScreenState extends State<BasalProfileDetailScreen> {
if (showConfirmationDialogOnCancel &&
(isNew &&
(_active ||
(_active != widget.active ||
_nameController.text != '' ||
_notesController.text != '')) ||
(!isNew &&

View File

@ -15,8 +15,15 @@ class BolusDetailScreen extends StatefulWidget {
final BolusProfile bolusProfile;
final Bolus? bolus;
final TimeOfDay? suggestedStartTime;
final TimeOfDay? suggestedEndTime;
const BolusDetailScreen({Key? key, required this.bolusProfile, this.bolus})
const BolusDetailScreen(
{Key? key,
required this.bolusProfile,
this.bolus,
this.suggestedStartTime,
this.suggestedEndTime})
: super(key: key);
@override
@ -39,6 +46,12 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
@override
void initState() {
super.initState();
if (widget.suggestedStartTime != null) {
_startTime = widget.suggestedStartTime!;
}
if (widget.suggestedEndTime != null) {
_endTime = widget.suggestedEndTime!;
}
if (widget.bolus != null) {
_startTime = TimeOfDay.fromDateTime(widget.bolus!.startTime);
_endTime = TimeOfDay.fromDateTime(widget.bolus!.endTime);
@ -60,25 +73,64 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
_endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime);
}
void updateMgPerDl() {
_mgPerDlController.text = Utils.convertMmolPerLToMgPerDl(
double.tryParse(_mmolPerLController.text) ?? 0)
.toString();
Future<String?> validateTimePeriod() async {
String? error;
List<Bolus> bolusRates =
await Bolus.fetchAllForBolusProfile(widget.bolusProfile);
// check for duplicates
if (bolusRates
.where((other) =>
(widget.bolus == null ||
widget.bolus!.objectId != other.objectId) &&
_startTime.hour == other.startTime.hour &&
_startTime.minute == other.startTime.minute)
.isNotEmpty) {
error = 'There\'s already a rate with this start time.';
}
void updateMmolPerL() {
_mmolPerLController.text = Utils.convertMgPerDlToMmolPerL(
int.tryParse(_mgPerDlController.text) ?? 0)
.toString();
if (bolusRates
.where((other) =>
(widget.bolus == null ||
widget.bolus!.objectId != other.objectId) &&
DateTimeUtils.convertTimeOfDayToDateTime(_startTime)
.isBefore(other.startTime) &&
DateTimeUtils.convertTimeOfDayToDateTime(_endTime)
.isAfter(other.startTime))
.isNotEmpty) {
error = 'This rate\'s time period overlaps with another one.';
}
return error == null
? null
: showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text(error!),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'CANCEL'),
child: const Text('GO BACK TO EDITING'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, 'CONFIRM'),
child: const Text('SAVE AS IS'),
),
],
);
});
}
void handleSaveAction() async {
// TODO: add confirmation dialog in case time period is already covered
if (_bolusForm.currentState!.validate()) {
await validateTimePeriod().then((value) async {
if (value != 'CANCEL') {
bool isNew = widget.bolus == null;
isNew
? await Bolus.save(
startTime: DateTimeUtils.convertTimeOfDayToDateTime(_startTime),
startTime:
DateTimeUtils.convertTimeOfDayToDateTime(_startTime),
endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime),
units: double.parse(_unitsController.text),
bolusProfile: widget.bolusProfile.objectId!,
@ -88,7 +140,8 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
)
: await Bolus.update(
widget.bolus!.objectId!,
startTime: DateTimeUtils.convertTimeOfDayToDateTime(_startTime),
startTime:
DateTimeUtils.convertTimeOfDayToDateTime(_startTime),
endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime),
units: double.tryParse(_unitsController.text),
carbs: double.tryParse(_carbsController.text),
@ -97,16 +150,19 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
);
Navigator.pop(context, '${isNew ? 'New' : ''} Bolus Rate saved');
}
});
}
}
void handleCancelAction() {
bool isNew = widget.bolus == null;
if (showConfirmationDialogOnCancel &&
((isNew &&
(_startTime.hour != 0 ||
_endTime.hour != 0 ||
_startTime.minute != 0 ||
_endTime.minute != 0 ||
(_startTime.hour != (widget.suggestedStartTime?.hour ?? 0) ||
_endTime.hour != (widget.suggestedEndTime?.hour ?? 0) ||
_startTime.minute !=
(widget.suggestedStartTime?.minute ?? 0) ||
_endTime.minute != (widget.suggestedEndTime?.minute ?? 0) ||
(double.tryParse(_unitsController.text) ?? 0) != 0.0 ||
(double.tryParse(_carbsController.text) ?? 0) != 0.0 ||
(int.tryParse(_mgPerDlController.text) ?? 0) != 0 ||
@ -181,7 +237,6 @@ class _BolusDetailScreenState extends State<BolusDetailScreen> {
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 5),
// TODO fix handling of time zones!
child: StyledTimeOfDayFormField(
label: 'Start Time',
controller: _startTimeController,

View File

@ -66,7 +66,7 @@ class _BolusListScreenState extends State<BolusListScreen> {
}
}
String? checkBolusValidity(List<Bolus> bolusRates, int index) {
String? validateTimePeriod(List<Bolus> bolusRates, int index) {
Bolus bolus = bolusRates[index];
// check for gaps
@ -88,7 +88,6 @@ class _BolusListScreenState extends State<BolusListScreen> {
}
// check for duplicates
if (bolusRates
.where((other) => bolus != other && bolus.startTime == other.startTime)
.isNotEmpty) {
@ -133,31 +132,28 @@ class _BolusListScreenState extends State<BolusListScreen> {
itemBuilder: (context, index) {
final bolus = snapshot.data![index];
final error =
checkBolusValidity(snapshot.data!, index);
validateTimePeriod(snapshot.data!, index);
return ListTile(
isThreeLine: true,
tileColor:
error != null ? Colors.red.shade100 : null,
onTap: () {
handleEditAction(bolus);
},
title: Row(
title: Text(
'${DateTimeUtils.displayTime(bolus.startTime)} - ${DateTimeUtils.displayTime(bolus.endTime)}'),
subtitle: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
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(
'${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'}'),
error != null
? Text(error,
style: const TextStyle(color: Colors.red))
: Container(),
style: const TextStyle(
color: Colors.red))
: const Text('')
]),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [

View File

@ -48,16 +48,7 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
}
addBolusButton = FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return BolusDetailScreen(bolusProfile: widget.bolusProfile!);
},
),
).then((message) => refresh(message: message));
},
onPressed: handleAddNew,
child: const Icon(Icons.add),
);
@ -163,6 +154,46 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
}
}
void handleAddNew() async {
List<Bolus> bolusRates =
await Bolus.fetchAllForBolusProfile(widget.bolusProfile!);
TimeOfDay? suggestedStartTime;
TimeOfDay? suggestedEndTime;
bolusRates.asMap().forEach((index, bolus) {
if (suggestedStartTime == null && suggestedEndTime == null) {
if (index == 0 &&
(bolus.startTime.hour != 0 || bolus.startTime.minute != 0)) {
suggestedStartTime = const TimeOfDay(hour: 0, minute: 0);
suggestedEndTime = TimeOfDay.fromDateTime(bolus.startTime);
} else if ((index == bolusRates.length - 1) &&
(bolus.endTime.hour != 0 || bolus.endTime.minute != 0)) {
suggestedStartTime = TimeOfDay.fromDateTime(bolus.endTime);
suggestedEndTime = const TimeOfDay(hour: 0, minute: 0);
} else if (index != 0) {
var lastEndTime = bolusRates[index - 1].endTime;
if (bolus.startTime.isAfter(lastEndTime)) {
suggestedStartTime = TimeOfDay.fromDateTime(lastEndTime);
suggestedEndTime = TimeOfDay.fromDateTime(bolus.startTime);
}
}
}
});
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return BolusDetailScreen(
bolusProfile: widget.bolusProfile!,
suggestedStartTime: suggestedStartTime,
suggestedEndTime: suggestedEndTime,
);
},
),
).then((message) => refresh(message: message));
}
void handleSaveAction() async {
if (_bolusProfileForm.currentState!.validate()) {
await checkActiveProfiles();
@ -187,7 +218,7 @@ class _BolusProfileDetailScreenState extends State<BolusProfileDetailScreen> {
if (showConfirmationDialogOnCancel &&
(isNew &&
(_active ||
(_active != widget.active ||
_nameController.text != '' ||
_notesController.text != '')) ||
(!isNew &&

View File

@ -133,48 +133,46 @@ class _ActiveLogEventListScreenState extends State<ActiveLogEventListScreen> {
return ViewWithProgressIndicator(
snapshot: snapshot,
child: snapshot.data == null || snapshot.data!.isEmpty
? const Padding(
padding: EdgeInsets.all(10.0),
child: Text('No Active Events'),
)
? Container()
: ListBody(
children: [
// TODO: fix problems that this futurebuilder in futurebuilder creates
FutureBuilder<List<LogEventType>>(
future: _logEventTypes,
builder: (context, types) {
return DataTable(
columnSpacing: 10.0,
showCheckboxColumn: false,
rows: snapshot.data != null
? snapshot.data!.map((event) {
return DataRow(
cells: event.asDataTableCells(
[
IconButton(
icon: const Icon(
Icons.stop),
iconSize: 16.0,
onPressed: () =>
handleStopAction(
event),
),
IconButton(
icon: const Icon(
Icons.delete),
iconSize: 16.0,
onPressed: () =>
handleDeleteAction(
event),
),
],
types: types.data,
),
);
}).toList()
: [],
columns: LogEvent.asDataTableColumns(),
);
// return DataTable(
// columnSpacing: 10.0,
// showCheckboxColumn: false,
// rows: snapshot.data != null
// ? snapshot.data!.map((event) {
// return DataRow(
// cells: event.asDataTableCells(
// [
// IconButton(
// icon: const Icon(
// Icons.stop),
// iconSize: 16.0,
// onPressed: () =>
// handleStopAction(
// event),
// ),
// IconButton(
// icon: const Icon(
// Icons.delete),
// iconSize: 16.0,
// onPressed: () =>
// handleDeleteAction(
// event),
// ),
// ],
// types: types.data,
// ),
// );
// }).toList()
// : [],
// columns: LogEvent.asDataTableColumns(),
// );
return Container();
})
],
),

View File

@ -5,6 +5,7 @@ import 'package:diameter/models/log_event.dart';
import 'package:diameter/models/log_event_type.dart';
import 'package:diameter/screens/log/active_log_event_list.dart';
import 'package:diameter/screens/log/log_event_detail.dart';
import 'package:diameter/utils/date_time_utils.dart';
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:diameter/components/progress_indicator.dart';
@ -80,7 +81,6 @@ class _LogEventListScreenState extends State<LogEventListScreen> {
@override
Widget build(BuildContext context) {
bool isNew = widget.logEntry == null;
return SingleChildScrollView(
padding: const EdgeInsets.only(top: 10.0),
child: Column(
@ -97,39 +97,51 @@ class _LogEventListScreenState extends State<LogEventListScreen> {
padding: EdgeInsets.all(10.0),
child: Text('No Events for this Log Entry'),
)
: ListBody(
children: [
FutureBuilder<List<LogEventType>>(
: FutureBuilder<List<LogEventType>>(
future: _logEventTypes,
builder: (context, types) {
return DataTable(
columnSpacing: 10.0,
showCheckboxColumn: false,
rows: snapshot.data != null
? snapshot.data!.map((event) {
return DataRow(
cells: event.asDataTableCells([
IconButton(
icon: const Icon(Icons.edit),
iconSize: 16.0,
onPressed: () =>
handleEditAction(event)),
IconButton(
icon:
const Icon(Icons.delete),
iconSize: 16.0,
onPressed: () =>
handleDeleteAction(
event)),
], types: types.data),
);
}).toList()
: [],
columns: LogEvent.asDataTableColumns(),
);
})
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data != null
? snapshot.data!.length
: 0,
itemBuilder: (context, index) {
final event = snapshot.data![index];
return ListTile(
onTap: () {
handleEditAction(event);
},
title: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(types.data
?.firstWhere((element) =>
element.objectId ==
event.eventType)
.value ??
'')),
],
),
subtitle: Text(
'${DateTimeUtils.displayDateTime(event.time)}${event.hasEndTime ? ' - ${DateTimeUtils.displayDateTime(event.endTime, fallback: '(ongoing)')}' : ''}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(
Icons.delete,
color: Colors.blue,
),
onPressed: () =>
handleDeleteAction(event),
),
],
),
);
},
);
}),
);
},
),
@ -140,39 +152,51 @@ class _LogEventListScreenState extends State<LogEventListScreen> {
snapshot: snapshot,
child: snapshot.data == null || snapshot.data!.isEmpty
? Container()
: ListBody(
children: [
FutureBuilder<List<LogEventType>>(
: FutureBuilder<List<LogEventType>>(
future: _logEventTypes,
builder: (context, types) {
return DataTable(
columnSpacing: 10.0,
showCheckboxColumn: false,
rows: snapshot.data != null
? snapshot.data!.map((event) {
return DataRow(
cells: event.asDataTableCells([
IconButton(
icon: const Icon(Icons.edit),
iconSize: 16.0,
onPressed: () =>
handleEditAction(event)),
IconButton(
icon:
const Icon(Icons.delete),
iconSize: 16.0,
onPressed: () =>
handleDeleteAction(
event)),
], types: types.data),
);
}).toList()
: [],
columns: LogEvent.asDataTableColumns(),
);
})
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data != null
? snapshot.data!.length
: 0,
itemBuilder: (context, index) {
final event = snapshot.data![index];
return ListTile(
onTap: () {
handleEditAction(event);
},
title: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(types.data
?.firstWhere((element) =>
element.objectId ==
event.eventType)
.value ??
'')),
],
),
subtitle: Text(
'${DateTimeUtils.displayDateTime(event.time)}${event.hasEndTime ? ' - ${DateTimeUtils.displayDateTime(event.endTime)}' : ''}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(
Icons.delete,
color: Colors.blue,
),
onPressed: () =>
handleDeleteAction(event),
),
],
),
);
},
);
}),
);
},
),

View File

@ -3,8 +3,6 @@ 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;