diameter/lib/screens/reports/export.dart
2022-03-21 01:08:05 +01:00

477 lines
15 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/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: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: const Text('single date'),
groupValue: exportRange,
value: false,
onChanged: (_) {
setState(() {
exportRange = false;
});
}),
),
Expanded(
child: RadioListTile(
title: const Text('range'),
groupValue: exportRange,
value: true,
onChanged: (value) {
setState(() {
exportRange = true;
});
}),
),
],
),
Row(
children: [
Expanded(
child: DateTimeFormField(
date: exportStartDate,
label: '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: 'End Date',
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: const Text('show Chart'),
controlAffinity: ListTileControlAffinity.leading,
),
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: CheckboxListTile(
value: showBolus,
onChanged: showChart
? (_) => setState(() => showBolus = !showBolus)
: null,
title: const Text('show Bolus'),
controlAffinity: ListTileControlAffinity.leading,
),
),
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: CheckboxListTile(
value: showBasal,
onChanged: showChart
? (_) => setState(() => showBasal = !showBasal)
: null,
title: const Text('show Basal'),
controlAffinity: ListTileControlAffinity.leading,
),
),
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: CheckboxListTile(
value: showMeals,
onChanged: showChart
? (_) => setState(() => showMeals = !showMeals)
: null,
title: const Text('show Meals'),
controlAffinity: ListTileControlAffinity.leading,
),
),
CheckboxListTile(
value: showTable,
onChanged: (_) => setState(() => showTable = !showTable),
title: const Text('show Table'),
controlAffinity: ListTileControlAffinity.leading,
),
],
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('CANCEL'),
),
ElevatedButton(
onPressed: !_isSaving && (showChart || showTable)
? () => onExport(context)
: null,
child: const Text('EXPORT'),
),
]);
}
}
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 = [
'Time',
'Glucose',
'Bolus',
'Notes',
'Meals',
'Portion Size',
'Carbohydrates',
'Meal Bolus',
'Meal Notes',
];
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} U${bolus.delay != null ? ' (over ${bolus.delay} min)' : ''}'
].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} U${bolus.delay != null ? ' (over ${bolus.delay} min)' : ''}${bolus.meal.target != null ? ' (for ${bolus.meal.target!.value})' : ''}')
.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} U'].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(
'LOG REPORT',
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();
}