482 lines
17 KiB
Dart
482 lines
17 KiB
Dart
import 'dart:io';
|
|
import 'dart:math';
|
|
import 'dart:typed_data';
|
|
import 'package:diameter/components/forms/date_time_form_field.dart';
|
|
import 'package:diameter/localization_keys.dart';
|
|
import 'package:diameter/models/glucose_target.dart';
|
|
import 'package:diameter/models/log_bolus.dart';
|
|
import 'package:diameter/models/log_entry.dart';
|
|
import 'package:diameter/models/log_event.dart';
|
|
import 'package:diameter/models/log_meal.dart';
|
|
import 'package:diameter/models/settings.dart';
|
|
import 'package:diameter/screens/reports/daily_chart.dart';
|
|
import 'package:diameter/utils/date_time_utils.dart';
|
|
import 'package:diameter/utils/utils.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_translate/flutter_translate.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:open_file/open_file.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:pdf/pdf.dart';
|
|
import 'package:pdf/widgets.dart' as pw;
|
|
import 'package:printing/printing.dart';
|
|
|
|
class MealData {
|
|
int id;
|
|
String text;
|
|
String portion;
|
|
String carbs;
|
|
String notes;
|
|
String boli;
|
|
|
|
MealData({
|
|
this.id = 0,
|
|
this.text = '',
|
|
this.portion = '',
|
|
this.carbs = '',
|
|
this.notes = '',
|
|
this.boli = '',
|
|
});
|
|
}
|
|
|
|
class ExportDialog extends StatefulWidget {
|
|
static const String routeName = '/export';
|
|
|
|
final DateTime? date;
|
|
|
|
const ExportDialog({Key? key, this.date}) : super(key: key);
|
|
|
|
@override
|
|
_ExportDialogState createState() => _ExportDialogState();
|
|
}
|
|
|
|
class _ExportDialogState extends State<ExportDialog> {
|
|
final ScrollController _scrollController = ScrollController();
|
|
|
|
late DateTime exportStartDate;
|
|
late DateTime exportEndDate;
|
|
|
|
TextEditingController exportStartDateController =
|
|
TextEditingController(text: '');
|
|
TextEditingController exportEndDateController =
|
|
TextEditingController(text: '');
|
|
|
|
bool _isSaving = false;
|
|
bool exportRange = false;
|
|
bool showChart = true;
|
|
bool showBolus = true;
|
|
bool showBasal = true;
|
|
bool showMeals = true;
|
|
bool showTable = true;
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
exportStartDate = widget.date ?? DateTimeUtils.today();
|
|
exportEndDate = DateTimeUtils.today();
|
|
|
|
exportStartDateController.text = DateTimeUtils.displayDate(exportStartDate);
|
|
exportEndDateController.text = DateTimeUtils.displayDate(exportEndDate);
|
|
}
|
|
|
|
void onExport(BuildContext context) async {
|
|
setState(() {
|
|
_isSaving = true;
|
|
});
|
|
final bytes = await generateLogReport(
|
|
startDate: exportStartDate,
|
|
endDate: exportRange ? exportEndDate : exportStartDate,
|
|
showChart: showChart,
|
|
showTable: showTable,
|
|
showMeals: showMeals,
|
|
showBasal: showBasal,
|
|
showBolus: showBolus,
|
|
);
|
|
final appDocDir = await getApplicationDocumentsDirectory();
|
|
final appDocPath = appDocDir.path;
|
|
|
|
final DateFormat formatter = DateFormat(DateFormat.YEAR_MONTH_DAY);
|
|
final file = File(
|
|
'$appDocPath/diameter_${exportRange ? '${formatter.format(exportStartDate)}-${formatter.format(exportEndDate)}' : formatter.format(exportStartDate)}.pdf');
|
|
await file.writeAsBytes(bytes);
|
|
await OpenFile.open(file.path);
|
|
Navigator.pop(context);
|
|
setState(() {
|
|
_isSaving = false;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AlertDialog(
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: RadioListTile(
|
|
title: Text(translate(LocalizationKeys.reports_export_singleDate)),
|
|
groupValue: exportRange,
|
|
value: false,
|
|
onChanged: (_) {
|
|
setState(() {
|
|
exportRange = false;
|
|
});
|
|
}),
|
|
),
|
|
Expanded(
|
|
child: RadioListTile(
|
|
title: Text(translate(LocalizationKeys.reports_export_range)),
|
|
groupValue: exportRange,
|
|
value: true,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
exportRange = true;
|
|
});
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: DateTimeFormField(
|
|
date: exportStartDate,
|
|
label: translate(LocalizationKeys.reports_export_date),
|
|
controller: exportStartDateController,
|
|
onChanged: (newDate) {
|
|
if (newDate != null) {
|
|
exportStartDate =
|
|
DateTime(newDate.year, newDate.month, newDate.day);
|
|
exportStartDateController.text =
|
|
DateTimeUtils.displayDate(exportStartDate);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
exportRange
|
|
? Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(left: 5.0),
|
|
child: DateTimeFormField(
|
|
date: exportEndDate,
|
|
label: translate(LocalizationKeys.reports_export_endDate),
|
|
controller: exportEndDateController,
|
|
onChanged: (newDate) {
|
|
if (newDate != null) {
|
|
exportEndDate = DateTime(
|
|
newDate.year, newDate.month, newDate.day);
|
|
exportEndDateController.text =
|
|
DateTimeUtils.displayDate(exportEndDate);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
)
|
|
: Container(),
|
|
],
|
|
),
|
|
CheckboxListTile(
|
|
value: showChart,
|
|
onChanged: (_) => setState(() => showChart = !showChart),
|
|
title: Text(translate(LocalizationKeys.reports_export_showChart)),
|
|
controlAffinity: ListTileControlAffinity.leading,
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 20.0),
|
|
child: CheckboxListTile(
|
|
value: showBolus,
|
|
onChanged: showChart
|
|
? (_) => setState(() => showBolus = !showBolus)
|
|
: null,
|
|
title: Text(translate(LocalizationKeys.reports_export_showBolus)),
|
|
controlAffinity: ListTileControlAffinity.leading,
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 20.0),
|
|
child: CheckboxListTile(
|
|
value: showBasal,
|
|
onChanged: showChart
|
|
? (_) => setState(() => showBasal = !showBasal)
|
|
: null,
|
|
title: Text(translate(LocalizationKeys.reports_export_showBasal)),
|
|
controlAffinity: ListTileControlAffinity.leading,
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 20.0),
|
|
child: CheckboxListTile(
|
|
value: showMeals,
|
|
onChanged: showChart
|
|
? (_) => setState(() => showMeals = !showMeals)
|
|
: null,
|
|
title: Text(translate(LocalizationKeys.reports_export_showMeals)),
|
|
controlAffinity: ListTileControlAffinity.leading,
|
|
),
|
|
),
|
|
CheckboxListTile(
|
|
value: showTable,
|
|
onChanged: (_) => setState(() => showTable = !showTable),
|
|
title: Text(translate(LocalizationKeys.reports_export_showTable)),
|
|
controlAffinity: ListTileControlAffinity.leading,
|
|
),
|
|
],
|
|
),
|
|
actions: <Widget>[
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text(translate(LocalizationKeys.general_cancel).toUpperCase()),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: !_isSaving && (showChart || showTable)
|
|
? () => onExport(context)
|
|
: null,
|
|
child: Text(translate(LocalizationKeys.reports_export_export).toUpperCase()),
|
|
),
|
|
]);
|
|
}
|
|
}
|
|
|
|
Future<pw.Image> generateChartAsImage(
|
|
DateTime date,
|
|
List<LogEntry> logEntries,
|
|
List<LogEvent> logEvents,
|
|
List<GlucoseTarget> targets,
|
|
bool showMeals,
|
|
bool showBasal,
|
|
bool showBolus) async {
|
|
final chart = generateChart(
|
|
date, logEntries, logEvents, targets, showMeals, showBasal, showBolus);
|
|
|
|
return pw.Image(
|
|
await WidgetWraper.fromWidget(
|
|
widget: chart,
|
|
constraints: BoxConstraints(
|
|
minWidth: PdfPageFormat.a4.landscape.availableWidth,
|
|
maxWidth: PdfPageFormat.a4.landscape.availableWidth,
|
|
minHeight: PdfPageFormat.a4.landscape.availableHeight / 2,
|
|
maxHeight: PdfPageFormat.a4.landscape.availableHeight,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
pw.Table generateTable(List<LogEntry> logEntries) {
|
|
const baseColor = PdfColors.grey;
|
|
const small = pw.TextStyle(fontSize: 8.0);
|
|
|
|
final tableHeaders = [
|
|
translate(LocalizationKeys.reports_export_tableHeaders_time),
|
|
translate(LocalizationKeys.reports_export_tableHeaders_glucose),
|
|
translate(LocalizationKeys.reports_export_tableHeaders_notes),
|
|
translate(LocalizationKeys.reports_export_tableHeaders_meals),
|
|
translate(LocalizationKeys.reports_export_tableHeaders_portionSize),
|
|
translate(LocalizationKeys.reports_export_tableHeaders_carbs),
|
|
translate(LocalizationKeys.reports_export_tableHeaders_mealBolus),
|
|
translate(LocalizationKeys.reports_export_tableHeaders_mealNotes),
|
|
];
|
|
|
|
final List<pw.TableRow> data = logEntries.map((logEntry) {
|
|
pw.TextStyle glucoseStyle = pw.TextStyle(
|
|
color: PdfColor.fromInt(GlucoseTarget.getColorForGlucose(
|
|
mgPerDl: logEntry.mgPerDl ?? 0, mmolPerL: logEntry.mmolPerL ?? 0)
|
|
.value),
|
|
);
|
|
|
|
final boli = LogBolus.getAllForEntry(logEntry.id);
|
|
final meals = LogMeal.getAllForEntry(logEntry.id)
|
|
.map((meal) => MealData(
|
|
id: meal.id,
|
|
text: meal.value,
|
|
portion:
|
|
'${Utils.getFractionDigitsLength(meal.amount) == 0 ? meal.amount.toInt().toString() : meal.amount} ${meal.mealPortionType.target?.value}',
|
|
carbs: Utils.displayNutritionAmount(meal.totalCarbs ?? 0,
|
|
cutExtraDigits: true),
|
|
boli: [
|
|
for (var bolus in boli
|
|
.where((bolus) => bolus.meal.targetId == meal.id)
|
|
.toList())
|
|
'${bolus.units} ${translate(LocalizationKeys.general_suffixes_units)} ${bolus.delay != null ? translate(LocalizationKeys.log_detail_tabs_bolus_delayedBy, args: {
|
|
"delay": '${bolus.delay} ${translate(LocalizationKeys.general_suffixes_mins)}'
|
|
}) : ''}'
|
|
].join(' + '),
|
|
notes: meal.notes ?? '',
|
|
))
|
|
.toList();
|
|
final glucoseBoli = boli
|
|
.where((bolus) =>
|
|
Settings.glucoseMeasurement == GlucoseMeasurement.mgPerDl
|
|
? bolus.mgPerDlCorrection != null
|
|
: bolus.mmolPerLCorrection != null)
|
|
.toList();
|
|
final otherMealBoli = boli
|
|
.where((bolus) =>
|
|
bolus.meal.targetId != 0 &&
|
|
!meals.any((meal) => meal.id == bolus.meal.targetId))
|
|
.map((bolus) =>
|
|
'${bolus.units} ${translate(LocalizationKeys.general_suffixes_units)} ${bolus.delay != null ? translate(LocalizationKeys.log_detail_tabs_bolus_delayedBy, args: {
|
|
"delay": '${bolus.delay} ${translate(LocalizationKeys.general_suffixes_mins)}'
|
|
}) : ''}')
|
|
.toList();
|
|
|
|
return pw.TableRow(
|
|
decoration: pw.BoxDecoration(
|
|
color: logEntries.indexOf(logEntry) % 2 == 0 ? PdfColors.grey200 : null,
|
|
border: const pw.Border(
|
|
bottom: pw.BorderSide(
|
|
color: baseColor,
|
|
width: .5,
|
|
),
|
|
),
|
|
),
|
|
children: [
|
|
// Time
|
|
pw.Text(DateTimeUtils.displayTime(logEntry.time)),
|
|
// Blood Glucose
|
|
pw.Row(
|
|
children: [
|
|
pw.Text(
|
|
Utils.displayGlucose(
|
|
mgPerDl: logEntry.mgPerDl, mmolPerL: logEntry.mmolPerL),
|
|
style: glucoseStyle,
|
|
),
|
|
logEntry.glucoseTrend != null
|
|
? pw.Transform.rotate(
|
|
angle: -(logEntry.glucoseTrend! * pi / 180),
|
|
child: pw.Icon(
|
|
const pw.IconData(0xe5d8),
|
|
color: glucoseStyle.color,
|
|
size: 10.0,
|
|
),
|
|
)
|
|
: pw.Container(),
|
|
],
|
|
),
|
|
// Bolus
|
|
pw.Text(
|
|
[for (var bolus in glucoseBoli) '${bolus.units} ${translate(LocalizationKeys.general_suffixes_units)}'].join(' + ')),
|
|
// Notes
|
|
pw.Text(logEntry.notes ?? '', style: small),
|
|
// Meals
|
|
pw.Text([for (var meal in meals) meal.text].join('\n'), style: small),
|
|
// Portion Size
|
|
pw.Text([for (var meal in meals) meal.portion].join('\n'),
|
|
style: small),
|
|
// Carbohydrates
|
|
pw.Text([for (var meal in meals) meal.carbs].join('\n'), style: small),
|
|
// Meal Bolus
|
|
pw.Text(
|
|
[
|
|
for (var meal in meals) meal.boli,
|
|
for (var bolus in otherMealBoli) bolus,
|
|
].join('\n'),
|
|
style: small),
|
|
// Meal Notes
|
|
pw.Text([for (var meal in meals) meal.notes].join('\n'), style: small),
|
|
],
|
|
);
|
|
}).toList();
|
|
|
|
return pw.Table(
|
|
border: null,
|
|
tableWidth: pw.TableWidth.max,
|
|
children: [
|
|
pw.TableRow(
|
|
decoration: const pw.BoxDecoration(
|
|
color: baseColor,
|
|
),
|
|
children: tableHeaders
|
|
.map((header) => pw.Text(
|
|
header,
|
|
style: pw.TextStyle(
|
|
color: PdfColors.white,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
))
|
|
.toList()),
|
|
] +
|
|
data,
|
|
);
|
|
}
|
|
|
|
Future<Uint8List> generateLogReport(
|
|
{required DateTime startDate,
|
|
DateTime? endDate,
|
|
bool showChart = true,
|
|
bool showTable = true,
|
|
bool showMeals = true,
|
|
bool showBasal = true,
|
|
bool showBolus = true}) async {
|
|
final List<GlucoseTarget> targets = GlucoseTarget.getAll();
|
|
|
|
final document = pw.Document();
|
|
|
|
final pageFormat = PdfPageFormat.a4.landscape.applyMargin(
|
|
left: 2.0 * PdfPageFormat.cm,
|
|
top: 2.0 * PdfPageFormat.cm,
|
|
right: 2.0 * PdfPageFormat.cm,
|
|
bottom: 2.0 * PdfPageFormat.cm);
|
|
|
|
final theme = pw.ThemeData.withFont(
|
|
base: await PdfGoogleFonts.robotoRegular(),
|
|
bold: await PdfGoogleFonts.robotoBold(),
|
|
icons: await PdfGoogleFonts.materialIcons(),
|
|
).copyWith(
|
|
defaultTextStyle: const pw.TextStyle(fontSize: 9.0),
|
|
);
|
|
|
|
for (DateTime date = startDate;
|
|
endDate != null && !endDate.isBefore(startDate) && !date.isAfter(endDate);
|
|
date = date.add(const Duration(days: 1))) {
|
|
List<LogEntry> logEntries = LogEntry.getAllForDate(date);
|
|
List<LogEvent> logEvents = LogEvent.getAllForDate(date);
|
|
|
|
pw.Widget? chart = showChart
|
|
? await generateChartAsImage(date, logEntries, logEvents, targets,
|
|
showMeals, showBasal, showBolus)
|
|
: pw.Container();
|
|
pw.Widget? table = showTable ? generateTable(logEntries) : pw.Container();
|
|
|
|
document.addPage(
|
|
pw.Page(
|
|
pageFormat: pageFormat,
|
|
theme: theme,
|
|
build: (context) {
|
|
return pw.Column(
|
|
children: [
|
|
pw.Text(
|
|
translate(LocalizationKeys.reports_export_title).toUpperCase(),
|
|
style: pw.TextStyle(
|
|
fontWeight: pw.FontWeight.bold,
|
|
fontSize: 12,
|
|
letterSpacing: 2.0,
|
|
),
|
|
),
|
|
pw.Text(
|
|
'for ${DateTimeUtils.displayDate(date)}'.toUpperCase(),
|
|
style: const pw.TextStyle(
|
|
letterSpacing: 2.0,
|
|
),
|
|
),
|
|
chart,
|
|
pw.SizedBox(height: 10),
|
|
table,
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
return await document.save();
|
|
}
|