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 { 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: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('CANCEL'), ), ElevatedButton( onPressed: !_isSaving && (showChart || showTable) ? () => onExport(context) : null, child: const Text('EXPORT'), ), ]); } } Future generateChartAsImage( DateTime date, List logEntries, List logEvents, List 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 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 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 generateLogReport( {required DateTime startDate, DateTime? endDate, bool showChart = true, bool showTable = true, bool showMeals = true, bool showBasal = true, bool showBolus = true}) async { final List 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 logEntries = LogEntry.getAllForDate(date); List 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(); }