diff --git a/lib/models/basal.dart b/lib/models/basal.dart index 3658c53..85f3525 100644 --- a/lib/models/basal.dart +++ b/lib/models/basal.dart @@ -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('objectId'); - startTime = object.get('startTime')!; - endTime = object.get('endTime')!; + startTime = object.get('startTime')!.toLocal(); + endTime = object.get('endTime')!.toLocal(); units = object.get('units')! / 100; basalProfile = object.get('basalProfile')!.get('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 asDataTableCells(List? actions) { - return [ - DataCell(Text(DateTimeUtils.displayTime(startTime))), - DataCell(Text(DateTimeUtils.displayTime(endTime))), - DataCell(Text('${units.toString()} U')), - DataCell( - Row( - children: actions ?? [], - ), - ), - ]; - } - - static List 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 asDataTableCells(List? actions) { + // return [ + // DataCell(Text(DateTimeUtils.displayTime(startTime))), + // DataCell(Text(DateTimeUtils.displayTime(endTime))), + // DataCell(Text('${units.toString()} U')), + // DataCell( + // Row( + // children: actions ?? [], + // ), + // ), + // ]; + // } + // + // static List 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'))), + // ]; + // } } diff --git a/lib/models/bolus.dart b/lib/models/bolus.dart index a73912a..0e4d92a 100644 --- a/lib/models/bolus.dart +++ b/lib/models/bolus.dart @@ -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('objectId'); - startTime = object.get('startTime')!; - endTime = object.get('endTime')!; + startTime = object.get('startTime')!.toLocal(); + endTime = object.get('endTime')!.toLocal(); units = object.get('units')! / 100; carbs = object.get('carbs')!.toDouble(); mgPerDl = object.get('mgPerDl') != null @@ -39,6 +39,7 @@ class Bolus extends DataTableContent { QueryBuilder query = QueryBuilder(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 asDataTableCells(List? 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 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 asDataTableCells(List? 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 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; + // } } diff --git a/lib/models/log_entry.dart b/lib/models/log_entry.dart index b5d686e..99d22c1 100644 --- a/lib/models/log_entry.dart +++ b/lib/models/log_entry.dart @@ -20,7 +20,7 @@ class LogEntry { LogEntry(ParseObject object) { objectId = object.get('objectId'); - time = object.get('time')!; + time = object.get('time')!.toLocal(); mgPerDl = object.get('mgPerDl') != null ? object.get('mgPerDl')!.toInt() : null; @@ -58,8 +58,8 @@ class LogEntry { static Future> fetchAllForRange(DateTimeRange range) async { QueryBuilder query = QueryBuilder(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) { diff --git a/lib/models/log_event.dart b/lib/models/log_event.dart index d0869ba..b035d1e 100644 --- a/lib/models/log_event.dart +++ b/lib/models/log_event.dart @@ -23,8 +23,8 @@ class LogEvent extends DataTableContent { object.get('endLogEntry')?.get('objectId'); eventType = object.get('eventType')!.get('objectId')!; - time = object.get('time')!; - endTime = object.get('endTime'); + time = object.get('time')!.toLocal(); + endTime = object.get('endTime')?.toLocal(); hasEndTime = object.get('hasEndTime')!; notes = object.get('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 asDataTableCells(List actions, - {List? 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 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 asDataTableCells(List actions, + // {List? 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 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'))), + // ]; + // } } diff --git a/lib/screens/basal/basal_detail.dart b/lib/screens/basal/basal_detail.dart index e686695..c632d76 100644 --- a/lib/screens/basal/basal_detail.dart +++ b/lib/screens/basal/basal_detail.dart @@ -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 { @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,24 +64,78 @@ class _BasalDetailScreenState extends State { _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); } + Future validateTimePeriod() async { + String? error; + List 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: [ + 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()) { - bool isNew = widget.basal == null; - isNew - ? await Basal.save( - 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), - endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime), - units: double.parse(_unitsController.text), - ); - Navigator.pop(context, '${isNew ? 'New' : ''} Basal Rate saved'); + await validateTimePeriod().then((value) async { + if (value != 'CANCEL') { + bool isNew = widget.basal == null; + isNew + ? await Basal.save( + 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), + endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime), + units: double.parse(_unitsController.text), + ); + Navigator.pop(context, '${isNew ? 'New' : ''} Basal Rate saved'); + } + }); } } @@ -76,10 +143,11 @@ class _BasalDetailScreenState extends State { 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 { Row( children: [ Expanded( - // TODO fix handling of time zones! child: Padding( padding: const EdgeInsets.only(right: 5), child: StyledTimeOfDayFormField( diff --git a/lib/screens/basal/basal_list.dart b/lib/screens/basal/basal_list.dart index f02d1eb..5d9a229 100644 --- a/lib/screens/basal/basal_list.dart +++ b/lib/screens/basal/basal_list.dart @@ -65,12 +65,12 @@ class _BasalListScreenState extends State { } } - String? checkBasalValidity(List basalRates, int index) { + String? validateTimePeriod(List 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 { } 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 { 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, diff --git a/lib/screens/basal/basal_profile_detail.dart b/lib/screens/basal/basal_profile_detail.dart index 4f1c4e5..d0ce9b1 100644 --- a/lib/screens/basal/basal_profile_detail.dart +++ b/lib/screens/basal/basal_profile_detail.dart @@ -48,16 +48,7 @@ class _BasalProfileDetailScreenState extends State { } 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 { } } + void handleAddNew() async { + List 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 { if (showConfirmationDialogOnCancel && (isNew && - (_active || + (_active != widget.active || _nameController.text != '' || _notesController.text != '')) || (!isNew && diff --git a/lib/screens/bolus/bolus_detail.dart b/lib/screens/bolus/bolus_detail.dart index 2f24da0..0c044f6 100644 --- a/lib/screens/bolus/bolus_detail.dart +++ b/lib/screens/bolus/bolus_detail.dart @@ -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 { @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,42 +73,84 @@ class _BolusDetailScreenState extends State { _endTimeController.text = DateTimeUtils.displayTimeOfDay(_endTime); } - void updateMgPerDl() { - _mgPerDlController.text = Utils.convertMmolPerLToMgPerDl( - double.tryParse(_mmolPerLController.text) ?? 0) - .toString(); - } + Future validateTimePeriod() async { + String? error; + List bolusRates = + await Bolus.fetchAllForBolusProfile(widget.bolusProfile); - void updateMmolPerL() { - _mmolPerLController.text = Utils.convertMgPerDlToMmolPerL( - int.tryParse(_mgPerDlController.text) ?? 0) - .toString(); + // 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.'; + } + + 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: [ + 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()) { - bool isNew = widget.bolus == null; - isNew - ? await Bolus.save( - startTime: DateTimeUtils.convertTimeOfDayToDateTime(_startTime), - endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime), - units: double.parse(_unitsController.text), - bolusProfile: widget.bolusProfile.objectId!, - carbs: double.parse(_carbsController.text), - mgPerDl: int.tryParse(_mgPerDlController.text), - mmolPerL: double.tryParse(_mmolPerLController.text), - ) - : await Bolus.update( - widget.bolus!.objectId!, - startTime: DateTimeUtils.convertTimeOfDayToDateTime(_startTime), - endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime), - units: double.tryParse(_unitsController.text), - carbs: double.tryParse(_carbsController.text), - mgPerDl: int.tryParse(_mgPerDlController.text), - mmolPerL: double.parse(_mmolPerLController.text), - ); - Navigator.pop(context, '${isNew ? 'New' : ''} Bolus Rate saved'); + await validateTimePeriod().then((value) async { + if (value != 'CANCEL') { + bool isNew = widget.bolus == null; + isNew + ? await Bolus.save( + startTime: + DateTimeUtils.convertTimeOfDayToDateTime(_startTime), + endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime), + units: double.parse(_unitsController.text), + bolusProfile: widget.bolusProfile.objectId!, + carbs: double.parse(_carbsController.text), + mgPerDl: int.tryParse(_mgPerDlController.text), + mmolPerL: double.tryParse(_mmolPerLController.text), + ) + : await Bolus.update( + widget.bolus!.objectId!, + startTime: + DateTimeUtils.convertTimeOfDayToDateTime(_startTime), + endTime: DateTimeUtils.convertTimeOfDayToDateTime(_endTime), + units: double.tryParse(_unitsController.text), + carbs: double.tryParse(_carbsController.text), + mgPerDl: int.tryParse(_mgPerDlController.text), + mmolPerL: double.parse(_mmolPerLController.text), + ); + Navigator.pop(context, '${isNew ? 'New' : ''} Bolus Rate saved'); + } + }); } } @@ -103,10 +158,11 @@ class _BolusDetailScreenState extends State { 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 { Expanded( child: Padding( padding: const EdgeInsets.only(right: 5), - // TODO fix handling of time zones! child: StyledTimeOfDayFormField( label: 'Start Time', controller: _startTimeController, diff --git a/lib/screens/bolus/bolus_list.dart b/lib/screens/bolus/bolus_list.dart index c7bd822..8ce34d0 100644 --- a/lib/screens/bolus/bolus_list.dart +++ b/lib/screens/bolus/bolus_list.dart @@ -66,7 +66,7 @@ class _BolusListScreenState extends State { } } - String? checkBolusValidity(List bolusRates, int index) { + String? validateTimePeriod(List bolusRates, int index) { Bolus bolus = bolusRates[index]; // check for gaps @@ -88,7 +88,6 @@ class _BolusListScreenState extends State { } // check for duplicates - if (bolusRates .where((other) => bolus != other && bolus.startTime == other.startTime) .isNotEmpty) { @@ -133,31 +132,28 @@ class _BolusListScreenState extends State { 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( - 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(), + title: Text( + '${DateTimeUtils.displayTime(bolus.startTime)} - ${DateTimeUtils.displayTime(bolus.endTime)}'), + subtitle: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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)) + : const Text('') + ]), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/screens/bolus/bolus_profile_detail.dart b/lib/screens/bolus/bolus_profile_detail.dart index 67dd802..27652b8 100644 --- a/lib/screens/bolus/bolus_profile_detail.dart +++ b/lib/screens/bolus/bolus_profile_detail.dart @@ -48,16 +48,7 @@ class _BolusProfileDetailScreenState extends State { } 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 { } } + void handleAddNew() async { + List 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 { if (showConfirmationDialogOnCancel && (isNew && - (_active || + (_active != widget.active || _nameController.text != '' || _notesController.text != '')) || (!isNew && diff --git a/lib/screens/log/active_log_event_list.dart b/lib/screens/log/active_log_event_list.dart index 8f69c85..f8d6f79 100644 --- a/lib/screens/log/active_log_event_list.dart +++ b/lib/screens/log/active_log_event_list.dart @@ -133,48 +133,46 @@ class _ActiveLogEventListScreenState extends State { 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>( 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(); }) ], ), diff --git a/lib/screens/log/log_event_list.dart b/lib/screens/log/log_event_list.dart index 3e5460e..9c06ecd 100644 --- a/lib/screens/log/log_event_list.dart +++ b/lib/screens/log/log_event_list.dart @@ -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 { @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 { padding: EdgeInsets.all(10.0), child: Text('No Events for this Log Entry'), ) - : ListBody( - children: [ - FutureBuilder>( - 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(), - ); - }) - ], - ), + : FutureBuilder>( + future: _logEventTypes, + builder: (context, types) { + 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 { snapshot: snapshot, child: snapshot.data == null || snapshot.data!.isEmpty ? Container() - : ListBody( - children: [ - FutureBuilder>( - 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(), - ); - }) - ], - ), + : FutureBuilder>( + future: _logEventTypes, + builder: (context, types) { + 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), + ), + ], + ), + ); + }, + ); + }), ); }, ), diff --git a/lib/utils/date_time_utils.dart b/lib/utils/date_time_utils.dart index b635c32..9023e30 100644 --- a/lib/utils/date_time_utils.dart +++ b/lib/utils/date_time_utils.dart @@ -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;