Commit 6e3bc5c

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-06-14 16:27:58
finish import export code migration
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent 48c3047
app/lib/components/dialoges/add_export_column_dialoge.dart
@@ -166,12 +166,16 @@ class _AddExportColumnDialogeState extends State<AddExportColumnDialoge>
                       sys: widget.settings.preferredPressureUnit.wrap(123),
                       dia: widget.settings.preferredPressureUnit.wrap(78),
                       pul: 65,
-                      // FIXME 'test note',
+                    );
+                    final note = Note(
+                      time: record.time,
+                      note: 'test note',
+                      color: Colors.red.value,
                     );
                     final formatter = (type == _FormatterType.record)
                       ? ScriptedFormatter(recordPattern ?? '')
                       : ScriptedTimeFormatter(timePattern ?? '');
-                    final text = formatter.encode(record);
+                    final text = formatter.encode(record, note, []);
                     final decoded = formatter.decode(text);
                     return Column(
                       children: [
app/lib/components/dialoges/import_preview_dialoge.dart
@@ -80,7 +80,7 @@ class _ImportPreviewDialogeState extends State<ImportPreviewDialoge> {
     onActionButtonPressed: (_showingError) ? null : () {
       final result = _actor.attemptParse();
       if (result.hasError()) return;
-      Navigator.pop<List<BloodPressureRecord>>(context, result.getOr((e) => null));
+      Navigator.pop<List<FullEntry>>(context, result.getOr((e) => null));
     },
     actions: [
       CheckboxMenuButton(
@@ -192,12 +192,12 @@ class _ImportPreviewDialogeState extends State<ImportPreviewDialoge> {
 }
 
 /// Shows a dialoge to preview import of a csv file
-Future<List<BloodPressureRecord>?> showImportPreview(
+Future<List<FullEntry>?> showImportPreview(
   BuildContext context,
   CsvRecordParsingActor initialActor,
   ExportColumnsManager columnsManager,
   bool bottomAppBar,) =>
-  showDialog<List<BloodPressureRecord>>(
+  showDialog<List<FullEntry>>(
     context: context, builder: (context) =>
     Dialog.fullscreen(
       child: ImportPreviewDialoge(
app/lib/model/export_import/column.dart
@@ -1,3 +1,7 @@
+import 'dart:convert';
+import 'dart:ui';
+
+import 'package:blood_pressure_app/model/blood_pressure/needle_pin.dart';
 import 'package:blood_pressure_app/model/export_import/export_configuration.dart';
 import 'package:blood_pressure_app/model/export_import/import_field_type.dart';
 import 'package:blood_pressure_app/model/export_import/record_formatter.dart';
@@ -18,73 +22,78 @@ class NativeColumn extends ExportColumn {
     systolic,
     diastolic,
     pulse,
-    /*notes, FIXME
+    notes,
     color,
-    needlePin,*/
+    needlePin,
   ];
+
   static final NativeColumn timestampUnixMs = NativeColumn._create(
-      'timestampUnixMs',
-      RowDataFieldType.timestamp,
-          (record) => record.time.millisecondsSinceEpoch.toString(),
-          (pattern) {
-        final value = int.tryParse(pattern);
-        return (value == null) ? null : DateTime.fromMillisecondsSinceEpoch(value);
-      }
-    );
-    static final NativeColumn systolic = NativeColumn._create(
-      'systolic',
-      RowDataFieldType.sys,
-      (record) => (record.sys?.mmHg).toString(),
-      int.tryParse,
-    );
-    static final NativeColumn diastolic = NativeColumn._create(
-      'diastolic',
-      RowDataFieldType.dia,
-      (record) => (record.dia?.mmHg).toString(),
-      int.tryParse,
-    );
-    static final NativeColumn pulse = NativeColumn._create(
-      'pulse',
-      RowDataFieldType.pul,
-      (record) => record.pul.toString(),
-      int.tryParse,
-    );
-    /*
-    static final NativeColumn notes = NativeColumn._create(
-      'notes',
-      RowDataFieldType.notes,
-      (record) => record.notes,
-      (pattern) => pattern,
-    );
-    static final NativeColumn color = NativeColumn._create(
-      'color',
-      RowDataFieldType.needlePin,
-      (record) => record.needlePin?.color.value.toString() ?? '',
-      (pattern) {
-        final value = int.tryParse(pattern);
-        if (value == null) return null;
-        return MeasurementNeedlePin(Color(value));
-      }
-    );
-    static final NativeColumn needlePin = NativeColumn._create(
-      'needlePin',
-      RowDataFieldType.needlePin,
-      (record) => jsonEncode(record.needlePin?.toMap()),
-      (pattern) {
-        try {
-          final json = jsonDecode(pattern);
-          if (json is! Map<String, dynamic>) return null;
-          return MeasurementNeedlePin.fromMap(json);
-        } on FormatException {
-          return null;
+    'timestampUnixMs',
+    RowDataFieldType.timestamp,
+    (record, _, __) => record.time.millisecondsSinceEpoch.toString(),
+    (pattern) {
+      final value = int.tryParse(pattern);
+      return (value == null) ? null : DateTime.fromMillisecondsSinceEpoch(value);
+    }
+  );
+  static final NativeColumn systolic = NativeColumn._create(
+    'systolic',
+    RowDataFieldType.sys,
+    (record, _, __) => (record.sys?.mmHg).toString(),
+    int.tryParse,
+  );
+  static final NativeColumn diastolic = NativeColumn._create(
+    'diastolic',
+    RowDataFieldType.dia,
+    (record, _, __) => (record.dia?.mmHg).toString(),
+    int.tryParse,
+  );
+  static final NativeColumn pulse = NativeColumn._create(
+    'pulse',
+    RowDataFieldType.pul,
+    (record, _, __) => record.pul.toString(),
+    int.tryParse,
+  );
+  static final NativeColumn notes = NativeColumn._create(
+    'notes',
+    RowDataFieldType.notes,
+    (_, note, __) => note.note ?? '',
+    (pattern) => pattern,
+  );
+  static final NativeColumn color = NativeColumn._create(
+    'color',
+    RowDataFieldType.needlePin,
+    (_, note, __) => note.color?.toString() ?? '',
+    (pattern) {
+      final value = int.tryParse(pattern);
+      if (value == null) return null;
+      return MeasurementNeedlePin(Color(value));
+    }
+  );
+  static final NativeColumn needlePin = NativeColumn._create(
+    'needlePin',
+    RowDataFieldType.needlePin,
+    (_, note, __) => '{"color":${note.color}',
+    (pattern) {
+      try {
+        final json = jsonDecode(pattern);
+        if (json is! Map<String, dynamic>) return null;
+        if (json.containsKey('color')) {
+          final value = json['color'];
+          return (value is int)
+            ? MeasurementNeedlePin(Color(value))
+            : null;
         }
+      } on FormatException {
+        // ignore
       }
-    );
-  */
+      return null;
+    }
+  );
   
   final String _csvTitle;
   final RowDataFieldType _restoreableType;
-  final String Function(BloodPressureRecord record) _encode;
+  final String Function(BloodPressureRecord record, Note note, List<MedicineIntake> intakes) _encode;
   final Object? Function(String pattern) _decode;
 
   @override
@@ -98,13 +107,14 @@ class NativeColumn extends ExportColumn {
   }
 
   @override
-  String encode(BloodPressureRecord record) => _encode(record);
+  String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes) =>
+    _encode(record, note, intakes);
 
   @override
   String? get formatPattern => null;
 
   @override
-  String get internalIdentifier => 'native.$csvTitle';
+  String get internalIdentifier => 'native.$csvTitle'; // TODO: why is this needed
 
   @override
   RowDataFieldType? get restoreAbleType => _restoreableType;
@@ -213,7 +223,8 @@ class BuildInColumn extends ExportColumn {
   (RowDataFieldType, dynamic)? decode(String pattern) => _formatter.decode(pattern);
 
   @override
-  String encode(BloodPressureRecord record) => _formatter.encode(record);
+  String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes) =>
+    _formatter.encode(record, note, intakes);
 
   @override
   String? get formatPattern => _formatter.formatPattern;
@@ -258,7 +269,8 @@ class UserColumn extends ExportColumn {
   (RowDataFieldType, dynamic)? decode(String pattern) => formatter.decode(pattern);
 
   @override
-  String encode(BloodPressureRecord record) => formatter.encode(record);
+  String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes) =>
+    formatter.encode(record, note, intakes);
 
   @override
   String? get formatPattern => formatter.formatPattern;
@@ -296,9 +308,9 @@ class TimeColumn extends ExportColumn {
   }
 
   @override
-  String encode(BloodPressureRecord record) {
+  String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes) {
     _formatter ??= ScriptedTimeFormatter(formatPattern);
-    return _formatter!.encode(record);
+    return _formatter!.encode(record, note, intakes);
   }
 
   @override
app/lib/model/export_import/csv_converter.dart
@@ -21,11 +21,11 @@ class CsvConverter {
   final ExportColumnsManager availableColumns;
 
   /// Create the contents of a csv file from passed records.
-  String create(List<BloodPressureRecord> records) {
+  String create(List<FullEntry> entries) {
     final columns = settings.exportFieldsConfiguration.getActiveColumns(availableColumns);
-    final table = records.map(
-      (record) => columns.map(
-        (column) => column.encode(record),
+    final table = entries.map(
+      (entry) => columns.map(
+        (column) => column.encode(entry.$1, entry.$2, entry.$3),
       ).toList(),
     ).toList();
 
@@ -105,7 +105,7 @@ class CsvConverter {
       List<ExportColumn?> parsers, [
         bool assumeHeadline = true,
       ]) {
-    final List<BloodPressureRecord> records = [];
+    final List<FullEntry> entries = [];
     int currentLineNumber = assumeHeadline ? 1 : 0;
     for (final currentLine in dataLines) {
       if (currentLine.length < parsers.length) {
@@ -137,22 +137,28 @@ class CsvConverter {
             (piece) => piece.$1 == RowDataFieldType.dia,)?.$2;
       final int? pul = recordPieces.firstWhereOrNull(
             (piece) => piece.$1 == RowDataFieldType.pul,)?.$2;
-      final String note = recordPieces.firstWhereOrNull(
+      final String noteText = recordPieces.firstWhereOrNull(
             (piece) => piece.$1 == RowDataFieldType.notes,)?.$2 ?? '';
       final MeasurementNeedlePin? needlePin = recordPieces.firstWhereOrNull(
             (piece) => piece.$1 == RowDataFieldType.needlePin,)?.$2;
 
-      records.add(BloodPressureRecord(
+      final record = BloodPressureRecord(
         time: timestamp,
         sys: sys?.asMMHg,
         dia: dia?.asMMHg,
         pul: pul,
-        /*FIXME: note, needlePin: needlePin*/));
+      );
+      final note = Note(
+        time: timestamp,
+        note: noteText,
+        color: needlePin?.color.value,
+      );
+      entries.add((record, note, []));
       currentLineNumber++;
     }
 
-    assert(records.length == dataLines.length, 'every line should have been parse');
-    return RecordParsingResult.ok(records);
+    assert(entries.length == dataLines.length, 'every line should have been parse');
+    return RecordParsingResult.ok(entries);
   }
 }
 
app/lib/model/export_import/export_configuration.dart
@@ -91,8 +91,8 @@ class ActiveExportColumnConfiguration extends ChangeNotifier {
         NativeColumn.systolic,
         NativeColumn.diastolic,
         NativeColumn.pulse,
-        /*NativeColumn.notes, FIXME
-        NativeColumn.needlePin,*/
+        NativeColumn.notes,
+        NativeColumn.needlePin,
       ],
       ExportImportPreset.myHeart => [
         BuildInColumn.mhDate,
app/lib/model/export_import/import_field_type.dart
@@ -16,7 +16,7 @@ enum RowDataFieldType {
   /// Guarantees [String] is returned.
   notes,
   /// Guarantees that the returned type is of type [MeasurementNeedlePin].
-  needlePin;
+  needlePin; // TODO: replace with color
 
   /// Selection of a displayable string from [localizations].
   String localize(AppLocalizations localizations) {
app/lib/model/export_import/pdf_converter.dart
@@ -30,16 +30,16 @@ class PdfConverter {
   final ExportColumnsManager availableColumns;
 
   /// Create a pdf from a record list.
-  Future<Uint8List> create(List<BloodPressureRecord> records) async {
+  Future<Uint8List> create(List<FullEntry> entries) async {
     final pdf = pw.Document(
       creator: 'Blood pressure app',
     );
-    final analyzer = BloodPressureAnalyser(records.toList());
+    final analyzer = BloodPressureAnalyser(entries.records);
 
     pdf.addPage(pw.MultiPage(
       pageFormat: PdfPageFormat.a4,
       build: (pw.Context context) {
-        final title = (pdfSettings.exportTitle) ? _buildPdfTitle(records, analyzer) : null;
+        final title = (pdfSettings.exportTitle) ? _buildPdfTitle(analyzer) : null;
         title?.layout(context, const pw.BoxConstraints());
         final statistics = (pdfSettings.exportStatistics) ? _buildPdfStatistics(analyzer) : null;
         statistics?.layout(context, const pw.BoxConstraints());
@@ -52,7 +52,7 @@ class PdfConverter {
           if (pdfSettings.exportStatistics)
             statistics!,
           if (pdfSettings.exportData)
-            _buildPdfTable(records, availableHeight),
+            _buildPdfTable(entries, availableHeight),
         ];
       },
       maxPages: 100,
@@ -60,19 +60,19 @@ class PdfConverter {
     return pdf.save();
   }
 
-  pw.Widget _buildPdfTitle(List<BloodPressureRecord> records, BloodPressureAnalyser analyzer) {
-    if (records.length < 2) return pw.Text(localizations.errNoData);
+  pw.Widget _buildPdfTitle(BloodPressureAnalyser analyzer) {
+    if (analyzer.count < 2) return pw.Text(localizations.errNoData);
     final dateFormatter = DateFormat(settings.dateFormatString);
     return pw.Container(
-        child: pw.Text(
-            localizations.pdfDocumentTitle(
-                dateFormatter.format(analyzer.firstDay!),
-                dateFormatter.format(analyzer.lastDay!),
-            ),
-            style: const pw.TextStyle(
-              fontSize: 16,
-            ),
+      child: pw.Text(
+        localizations.pdfDocumentTitle(
+          dateFormatter.format(analyzer.firstDay!),
+          dateFormatter.format(analyzer.lastDay!),
         ),
+        style: const pw.TextStyle(
+          fontSize: 16,
+        ),
+      ),
     );
   }
 
@@ -88,12 +88,12 @@ class PdfConverter {
       ),
     );
 
-  pw.Widget _buildPdfTable(Iterable<BloodPressureRecord> records, double availableHeightOnFirstPage) {
+  pw.Widget _buildPdfTable(Iterable<FullEntry> entries, double availableHeightOnFirstPage) {
     final columns = pdfSettings.exportFieldsConfiguration.getActiveColumns(availableColumns);
 
-    final data = records.map(
-      (record) => columns.map(
-        (column) => column.encode(record),
+    final data = entries.map(
+      (entry) => columns.map(
+        (column) => column.encode(entry.$1, entry.$2, entry.$3),
       ).toList(),
     ).toList();
 
app/lib/model/export_import/record_formatter.dart
@@ -16,7 +16,7 @@ abstract interface class Formatter {
   ///
   /// There is no guarantee that the information in the record can be restored.
   /// If not null this must follow [formatPattern].
-  String encode(BloodPressureRecord record);
+  String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes);
 
   /// Type of data that can be restored from a string obtained by [encode].
   RowDataFieldType? get restoreAbleType;
@@ -71,7 +71,7 @@ class ScriptedFormatter implements Formatter {
   }
 
   @override
-  String encode(BloodPressureRecord record) {
+  String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes) {
     var fieldContents = pattern;
 
     // variables
@@ -79,8 +79,8 @@ class ScriptedFormatter implements Formatter {
     fieldContents = fieldContents.replaceAll(r'$SYS', (record.sys?.mmHg).toString());
     fieldContents = fieldContents.replaceAll(r'$DIA', (record.dia?.mmHg).toString());
     fieldContents = fieldContents.replaceAll(r'$PUL', record.pul.toString());
-    /*fieldContents = fieldContents.replaceAll(r'$NOTE', record.notes); FIXME
-    fieldContents = fieldContents.replaceAll(r'$COLOR', jsonEncode(record.needlePin?.toMap()));*/
+    fieldContents = fieldContents.replaceAll(r'$NOTE', note.note ?? '');
+    fieldContents = fieldContents.replaceAll(r'$COLOR', note.color?.toString() ?? '');
 
     // math
     fieldContents = fieldContents.replaceAllMapped(RegExp(r'\{\{([^}]*)}}'), (m) {
@@ -193,7 +193,8 @@ class ScriptedTimeFormatter implements Formatter {
   }
 
   @override
-  String encode(BloodPressureRecord record) => _timeFormatter.format(record.time);
+  String encode(BloodPressureRecord record, Note note, List<MedicineIntake> intakes) =>
+    _timeFormatter.format(record.time);
 
   @override
   String? get formatPattern => _timeFormatter.pattern;
app/lib/model/export_import/record_parsing_result.dart
@@ -7,16 +7,16 @@ import 'package:health_data_store/health_data_store.dart';
 class RecordParsingResult {
 
   /// Pass a valid record list and indicate success.
-  factory RecordParsingResult.ok(List<BloodPressureRecord> result)=>
+  factory RecordParsingResult.ok(List<FullEntry> result)=>
       RecordParsingResult._create(result, null);
 
   /// Indicate a parsing failure.
-  factory RecordParsingResult.err(RecordParsingError error)=>
+  factory RecordParsingResult.err(EntryParsingError error)=>
       RecordParsingResult._create(null, error);
   RecordParsingResult._create(this._result, this._error);
 
-  final List<BloodPressureRecord>? _result;
-  final RecordParsingError? _error;
+  final List<FullEntry>? _result;
+  final EntryParsingError? _error;
 
   /// Returns if there is an error present.
   ///
@@ -24,13 +24,13 @@ class RecordParsingResult {
   bool hasError() => _error != null;
 
   /// The returned error, if present.
-  RecordParsingError? get error => _error;
+  EntryParsingError? get error => _error;
 
   /// Returns the passed list on success or the result of [errorHandler] in case
   /// a error is present.
   ///
   /// When [errorHandler] returns null a empty list is passed.
-  List<BloodPressureRecord> getOr(List<BloodPressureRecord>? Function(RecordParsingError error) errorHandler) {
+  List<FullEntry> getOr(List<FullEntry>? Function(EntryParsingError error) errorHandler) {
     if (_result != null) {
       assert(_error == null);
       return _result!;
@@ -41,7 +41,7 @@ class RecordParsingResult {
 }
 
 /// Indicates what type error occurred while trying to decode a csv data.
-sealed class RecordParsingError {
+sealed class EntryParsingError {
   /// Create the localized String explaining this error
   String localize(AppLocalizations localizations) => switch (this) {
     RecordParsingErrorEmptyFile() => localizations.errParseEmptyCsvFile,
@@ -57,14 +57,14 @@ sealed class RecordParsingError {
 }
 
 /// There are not enough lines in the csv file to parse the record.
-class RecordParsingErrorEmptyFile extends RecordParsingError {}
+class RecordParsingErrorEmptyFile extends EntryParsingError {}
 
 /// There is no column that allows restoring a timestamp.
 // TODO: return line.
-class RecordParsingErrorTimeNotRestoreable extends RecordParsingError {}
+class RecordParsingErrorTimeNotRestoreable extends EntryParsingError {}
 
 /// There is no column with this csv title that can be reversed.
-class RecordParsingErrorUnknownColumn extends RecordParsingError {
+class RecordParsingErrorUnknownColumn extends EntryParsingError {
   RecordParsingErrorUnknownColumn(this.title);
   
   /// CSV title of the column no equivalent was found for. 
@@ -72,7 +72,7 @@ class RecordParsingErrorUnknownColumn extends RecordParsingError {
 }
 
 /// The current line has less fields than the first line.
-class RecordParsingErrorExpectedMoreFields extends RecordParsingError {
+class RecordParsingErrorExpectedMoreFields extends EntryParsingError {
   RecordParsingErrorExpectedMoreFields(this.lineNumber);
 
   /// Line in which this error occurred. 
@@ -80,7 +80,7 @@ class RecordParsingErrorExpectedMoreFields extends RecordParsingError {
 }
 
 /// The corresponding column couldn't decode a specific field in the csv file.
-class RecordParsingErrorUnparsableField extends RecordParsingError {
+class RecordParsingErrorUnparsableField extends EntryParsingError {
   RecordParsingErrorUnparsableField(this.lineNumber, this.fieldContents);
 
   /// Line in which this error occurred.
app/lib/screens/subsettings/export_import/export_button_bar.dart
@@ -1,4 +1,5 @@
 import 'dart:async';
+import 'dart:collection';
 import 'dart:convert';
 import 'dart:io';
 import 'dart:typed_data';
@@ -87,18 +88,36 @@ class ExportButtonBar extends StatelessWidget {
                       Provider.of<Settings>(context, listen: false).bottomAppBars,
                     );
                     if (importedRecords == null || !context.mounted) return;
-                    final repo = context.read<BloodPressureRepository>();
-                    await Future.forEach<BloodPressureRecord>(importedRecords, repo.add);
+                    final bpRepo = RepositoryProvider.of<BloodPressureRepository>(context);
+                    final noteRepo = RepositoryProvider.of<NoteRepository>(context);
+                    final intakeRepo = RepositoryProvider.of<MedicineIntakeRepository>(context);
+                    await Future.forEach<FullEntry>(importedRecords, (e) async {
+                      await bpRepo.add(e.$1);
+                      await noteRepo.add(e.$2);
+                      if (e.$3.isNotEmpty) {
+                        await Future.forEach(e.$3, intakeRepo.add);
+                      }
+                    });
                     messenger.showSnackBar(SnackBar(content: Text(
                       localizations.importSuccess(importedRecords.length),),),);
                     break;
                   case 'db':
                     if (file.path == null) return;
-                    final repo = context.read<BloodPressureRepository>();
+                    final bpRepo = RepositoryProvider.of<BloodPressureRepository>(context);
+                    final noteRepo = RepositoryProvider.of<NoteRepository>(context);
+                    final intakeRepo = RepositoryProvider.of<MedicineIntakeRepository>(context);
                     final importedDB = await HealthDataStore.load(await openReadOnlyDatabase(file.path!));
                     await Future.forEach(
                       await importedDB.bpRepo.get(DateRange.all()),
-                      repo.add,
+                      bpRepo.add,
+                    );
+                    await Future.forEach(
+                      await importedDB.noteRepo.get(DateRange.all()),
+                      noteRepo.add,
+                    );
+                    await Future.forEach(
+                      await importedDB.intakeRepo.get(DateRange.all()),
+                      intakeRepo.add,
                     );
                     break;
                   default:
@@ -133,7 +152,7 @@ void performExport(BuildContext context, [AppLocalizations? localizations]) asyn
         Provider.of<CsvExportSettings>(context, listen: false),
         Provider.of<ExportColumnsManager>(context, listen: false),
       );
-      final csvString = csvConverter.create(await _getRecords(context));
+      final csvString = csvConverter.create(await _getEntries(context));
       final data = Uint8List.fromList(utf8.encode(csvString));
       if (context.mounted) await _exportData(context, data, '$filename.csv', 'text/csv');
       break;
@@ -144,16 +163,52 @@ void performExport(BuildContext context, [AppLocalizations? localizations]) asyn
           Provider.of<Settings>(context, listen: false),
           Provider.of<ExportColumnsManager>(context, listen: false),
       );
-      final pdf = await pdfConverter.create(await _getRecords(context));
+      final pdf = await pdfConverter.create(await _getEntries(context));
       if (context.mounted) await _exportData(context, pdf, '$filename.pdf', 'text/pdf');
   }
 }
 
 /// Get the records that should be exported.
-Future<List<BloodPressureRecord>> _getRecords(BuildContext context) {
+Future<List<FullEntry>> _getEntries(BuildContext context) async {
+  // TODO: move function somewhere more practical
   final range = Provider.of<IntervallStoreManager>(context, listen: false).exportPage.currentRange;
-  final model = RepositoryProvider.of<BloodPressureRepository>(context);
-  return model.get(range);
+
+  final bpRepo = RepositoryProvider.of<BloodPressureRepository>(context);
+  final noteRepo = RepositoryProvider.of<NoteRepository>(context);
+  final intakeRepo = RepositoryProvider.of<MedicineIntakeRepository>(context);
+
+  final records = await bpRepo.get(range);
+  final notes = await noteRepo.get(range);
+  final intakes = await intakeRepo.get(range);
+
+  final Map<DateTime, (BloodPressureRecord?, Note?, List<MedicineIntake>)> entryMap = HashMap();
+  for (final r in records) {
+    assert(!entryMap.containsKey(r.time), 'multiple records at same time');
+    entryMap[r.time] = (r, null, []);
+  }
+  for (final n in notes) {
+    if(entryMap.containsKey(n.time)) {
+      assert(entryMap[n.time]!.$2 != null, 'multiple notes at same time');
+      entryMap[n.time] = (entryMap[n.time]!.$1, n, []);
+    } else {
+      entryMap[n.time] = (BloodPressureRecord(time: n.time), n, []);
+    }
+  }
+  for (final i in intakes) {
+    if(entryMap.containsKey(i.time)) {
+      entryMap[i.time]!.$3.add(i);
+    } else {
+      entryMap[i.time] = (null, null, [i]);
+    }
+  }
+  return entryMap
+    .values
+    .map<FullEntry>((e) {
+      // One of the values must exist or else this wouldn't be in the map.
+      final time = e.$1?.time ?? e.$2?.time ?? e.$3.firstOrNull?.time;
+      return (e.$1 ?? BloodPressureRecord(time: time!), e.$2 ?? Note(time: time!), e.$3);
+    })
+    .toList();
 }
 
 /// Save to default export path or share by providing a path.
app/test/model/export_import/csv_converter_test.dart
@@ -306,7 +306,7 @@ List<BloodPressureRecord> createRecords([int count = 20]) => [
         i, 100+i, 200+1, 'note $i', Color(123+i),),
 ];
 
-List<BloodPressureRecord>? failParse(RecordParsingError error) {
+List<BloodPressureRecord>? failParse(EntryParsingError error) {
   switch (error) {
     case RecordParsingErrorEmptyFile():
       fail('Parsing failed due to insufficient data.');
app/pubspec.lock
@@ -5,34 +5,39 @@ packages:
     dependency: transitive
     description:
       name: _fe_analyzer_shared
-      sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
+      sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3"
       url: "https://pub.dev"
     source: hosted
-    version: "67.0.0"
+    version: "68.0.0"
+  _macros:
+    dependency: transitive
+    description: dart
+    source: sdk
+    version: "0.1.5"
   analyzer:
     dependency: transitive
     description:
       name: analyzer
-      sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
+      sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808"
       url: "https://pub.dev"
     source: hosted
-    version: "6.4.1"
+    version: "6.5.0"
   archive:
     dependency: transitive
     description:
       name: archive
-      sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
+      sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
       url: "https://pub.dev"
     source: hosted
-    version: "3.4.10"
+    version: "3.6.1"
   args:
     dependency: transitive
     description:
       name: args
-      sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
+      sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.2"
+    version: "2.5.0"
   async:
     dependency: transitive
     description:
@@ -45,10 +50,10 @@ packages:
     dependency: transitive
     description:
       name: barcode
-      sha256: "91b143666f7bb13636f716b6d4e412e372ab15ff7969799af8c9e30a382e9385"
+      sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.6"
+    version: "2.2.8"
   bidi:
     dependency: transitive
     description:
@@ -93,10 +98,10 @@ packages:
     dependency: transitive
     description:
       name: built_value
-      sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e
+      sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb
       url: "https://pub.dev"
     source: hosted
-    version: "8.9.1"
+    version: "8.9.2"
   characters:
     dependency: transitive
     description:
@@ -234,10 +239,10 @@ packages:
     dependency: "direct main"
     description:
       name: flutter_bloc
-      sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2
+      sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
       url: "https://pub.dev"
     source: hosted
-    version: "8.1.5"
+    version: "8.1.6"
   flutter_lints:
     dependency: "direct dev"
     description:
@@ -255,18 +260,18 @@ packages:
     dependency: "direct main"
     description:
       name: flutter_markdown
-      sha256: cb44f7831b23a6bdd0f501718b0d2e8045cbc625a15f668af37ddb80314821db
+      sha256: "04c4722cc36ec5af38acc38ece70d22d3c2123c61305d555750a091517bbe504"
       url: "https://pub.dev"
     source: hosted
-    version: "0.6.21"
+    version: "0.6.23"
   flutter_plugin_android_lifecycle:
     dependency: transitive
     description:
       name: flutter_plugin_android_lifecycle
-      sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da
+      sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.17"
+    version: "2.0.20"
   flutter_reactive_ble:
     dependency: "direct main"
     description:
@@ -289,10 +294,10 @@ packages:
     dependency: "direct main"
     description:
       name: fluttertoast
-      sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1
+      sha256: "7eae679e596a44fdf761853a706f74979f8dd3cd92cf4e23cae161fda091b847"
       url: "https://pub.dev"
     source: hosted
-    version: "8.2.4"
+    version: "8.2.6"
   freezed_annotation:
     dependency: transitive
     description:
@@ -313,10 +318,10 @@ packages:
     dependency: transitive
     description:
       name: functional_data
-      sha256: aefdec4365452283b2a7cf420a3169654d51d3e9553069a22d76680d7a9d7c3d
+      sha256: "76d17dc707c40e552014f5a49c0afcc3f1e3f05e800cd6b7872940bfe41a5039"
       url: "https://pub.dev"
     source: hosted
-    version: "1.1.1"
+    version: "1.2.0"
   glob:
     dependency: transitive
     description:
@@ -352,10 +357,10 @@ packages:
     dependency: transitive
     description:
       name: image
-      sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
+      sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
       url: "https://pub.dev"
     source: hosted
-    version: "4.1.7"
+    version: "4.2.0"
   intl:
     dependency: "direct main"
     description:
@@ -364,14 +369,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.19.0"
-  js:
-    dependency: transitive
-    description:
-      name: js
-      sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
-      url: "https://pub.dev"
-    source: hosted
-    version: "0.6.7"
   jsaver:
     dependency: "direct main"
     description:
@@ -384,10 +381,10 @@ packages:
     dependency: transitive
     description:
       name: json_annotation
-      sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
+      sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
       url: "https://pub.dev"
     source: hosted
-    version: "4.8.1"
+    version: "4.9.0"
   leak_tracker:
     dependency: transitive
     description:
@@ -428,6 +425,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.2.0"
+  macros:
+    dependency: transitive
+    description:
+      name: macros
+      sha256: a8403c89b36483b4cbf9f1fcd24562f483cb34a5c9bf101cf2b0d8a083cf1239
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.1.0-main.5"
   markdown:
     dependency: transitive
     description:
@@ -560,18 +565,18 @@ packages:
     dependency: transitive
     description:
       name: permission_handler_android
-      sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474"
+      sha256: b29a799ca03be9f999aa6c39f7de5209482d638e6f857f6b93b0875c618b7e54
       url: "https://pub.dev"
     source: hosted
-    version: "12.0.5"
+    version: "12.0.7"
   permission_handler_apple:
     dependency: transitive
     description:
       name: permission_handler_apple
-      sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662
+      sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
       url: "https://pub.dev"
     source: hosted
-    version: "9.4.4"
+    version: "9.4.5"
   permission_handler_html:
     dependency: transitive
     description:
@@ -608,10 +613,10 @@ packages:
     dependency: transitive
     description:
       name: platform
-      sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
+      sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.4"
+    version: "3.1.5"
   plugin_platform_interface:
     dependency: transitive
     description:
@@ -620,14 +625,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.8"
-  pointycastle:
-    dependency: transitive
-    description:
-      name: pointycastle
-      sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
-      url: "https://pub.dev"
-    source: hosted
-    version: "3.7.4"
   protobuf:
     dependency: transitive
     description:
@@ -688,26 +685,26 @@ packages:
     dependency: "direct main"
     description:
       name: shared_preferences
-      sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
+      sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.2"
+    version: "2.2.3"
   shared_preferences_android:
     dependency: transitive
     description:
       name: shared_preferences_android
-      sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
+      sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577"
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.1"
+    version: "2.2.3"
   shared_preferences_foundation:
     dependency: transitive
     description:
       name: shared_preferences_foundation
-      sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
+      sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.5"
+    version: "2.4.0"
   shared_preferences_linux:
     dependency: transitive
     description:
@@ -765,34 +762,34 @@ packages:
     dependency: "direct main"
     description:
       name: sqflite
-      sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6
+      sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.2"
+    version: "2.3.3+1"
   sqflite_common:
     dependency: transitive
     description:
       name: sqflite_common
-      sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
+      sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
       url: "https://pub.dev"
     source: hosted
-    version: "2.5.3"
+    version: "2.5.4"
   sqflite_common_ffi:
     dependency: "direct dev"
     description:
       name: sqflite_common_ffi
-      sha256: "754927d82de369a6b9e760fb60640aa81da650f35ffd468d5a992814d6022908"
+      sha256: "4d6137c29e930d6e4a8ff373989dd9de7bac12e3bc87bce950f6e844e8ad3bb5"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.2+1"
+    version: "2.3.3"
   sqlite3:
     dependency: transitive
     description:
       name: sqlite3
-      sha256: "072128763f1547e3e9b4735ce846bfd226d68019ccda54db4cd427b12dfdedc9"
+      sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.0"
+    version: "2.4.3"
   sqlparser:
     dependency: "direct main"
     description:
@@ -869,26 +866,26 @@ packages:
     dependency: "direct main"
     description:
       name: url_launcher
-      sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e"
+      sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
       url: "https://pub.dev"
     source: hosted
-    version: "6.2.5"
+    version: "6.3.0"
   url_launcher_android:
     dependency: transitive
     description:
       name: url_launcher_android
-      sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745
+      sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
       url: "https://pub.dev"
     source: hosted
-    version: "6.3.0"
+    version: "6.3.3"
   url_launcher_ios:
     dependency: transitive
     description:
       name: url_launcher_ios
-      sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5"
+      sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
       url: "https://pub.dev"
     source: hosted
-    version: "6.2.5"
+    version: "6.3.0"
   url_launcher_linux:
     dependency: transitive
     description:
@@ -901,10 +898,10 @@ packages:
     dependency: transitive
     description:
       name: url_launcher_macos
-      sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
+      sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.0"
+    version: "3.2.0"
   url_launcher_platform_interface:
     dependency: transitive
     description:
@@ -917,10 +914,10 @@ packages:
     dependency: transitive
     description:
       name: url_launcher_web
-      sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d"
+      sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.0"
+    version: "2.3.1"
   url_launcher_windows:
     dependency: transitive
     description:
@@ -965,10 +962,10 @@ packages:
     dependency: transitive
     description:
       name: win32
-      sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480"
+      sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
       url: "https://pub.dev"
     source: hosted
-    version: "5.3.0"
+    version: "5.5.1"
   xdg_directories:
     dependency: transitive
     description:
@@ -994,5 +991,5 @@ packages:
     source: hosted
     version: "3.1.2"
 sdks:
-  dart: ">=3.3.0 <4.0.0"
-  flutter: ">=3.19.0"
+  dart: ">=3.4.0 <4.0.0"
+  flutter: ">=3.22.0"