383 lines
12 KiB
Dart
383 lines
12 KiB
Dart
// SPDX-FileCopyrightText: © 2025 Nøkken.io <nokken.io@proton.me>
|
|
// SPDX-License-Identifier: AGPL-3.0
|
|
//
|
|
// bloodwork_data_generator.dart
|
|
//
|
|
import 'dart:math' show Random;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:nokken/src/core/services/database/database_service.dart';
|
|
import 'package:nokken/src/features/bloodwork_tracker/models/bloodwork.dart';
|
|
|
|
/// A tool class to generate historical bloodwork data
|
|
class BloodworkDataGenerator {
|
|
final DatabaseService databaseService;
|
|
|
|
BloodworkDataGenerator(this.databaseService);
|
|
|
|
/// Generate bloodwork data, including:
|
|
/// - Bloodwork labs every 70-110 days
|
|
/// - 2-4 doctor appointments throughout the year
|
|
/// - 1 surgery appointment
|
|
Future<void> generateBloodworkData() async {
|
|
final random = Random();
|
|
|
|
// Start date: 364 days ago from today
|
|
final endDate = DateTime.now();
|
|
final startDate = endDate.subtract(const Duration(days: 364));
|
|
|
|
debugPrint(
|
|
'Generating bloodwork data from ${startDate.toIso8601String()} to ${endDate.toIso8601String()}');
|
|
|
|
// Generate bloodwork labs
|
|
await _generateBloodworkLabs(startDate, endDate, random);
|
|
|
|
// Generate doctor appointments
|
|
await _generateDoctorAppointments(startDate, endDate, random);
|
|
|
|
// Generate a surgery appointment
|
|
await _generateSurgeryAppointment(startDate, endDate, random);
|
|
|
|
debugPrint('Successfully generated bloodwork data.');
|
|
}
|
|
|
|
/// Generate bloodwork labs every 70-110 days
|
|
Future<void> _generateBloodworkLabs(
|
|
DateTime startDate, DateTime endDate, Random random) async {
|
|
// Start a bit before the actual start date to ensure we have consistent coverage
|
|
DateTime currentDate = startDate.subtract(const Duration(days: 30));
|
|
int labCount = 0;
|
|
|
|
while (currentDate.isBefore(endDate)) {
|
|
// Add a random interval between 70-110 days
|
|
final interval = random.nextInt(41) + 70; // 70-110 days
|
|
currentDate = currentDate.add(Duration(days: interval));
|
|
|
|
// Only add if it's within our target range
|
|
if (currentDate.isAfter(startDate) && currentDate.isBefore(endDate)) {
|
|
// Generate a bloodwork entry
|
|
final bloodwork = _generateBloodworkEntry(currentDate, random);
|
|
await databaseService.insertBloodwork(bloodwork);
|
|
labCount++;
|
|
}
|
|
}
|
|
|
|
debugPrint('Generated $labCount bloodwork lab records');
|
|
}
|
|
|
|
/// Generate 2-4 doctor appointments throughout the year
|
|
Future<void> _generateDoctorAppointments(
|
|
DateTime startDate, DateTime endDate, Random random) async {
|
|
// Decide how many appointments (2-4)
|
|
final appointmentCount = random.nextInt(3) + 2; // 2-4
|
|
debugPrint('Generating $appointmentCount doctor appointments');
|
|
|
|
// Generate appointment dates (evenly spaced with some randomness)
|
|
final yearInDays = 364;
|
|
final appointmentDates = <DateTime>[];
|
|
|
|
for (int i = 0; i < appointmentCount; i++) {
|
|
// Calculate roughly evenly spaced dates with some randomness
|
|
final targetDay = ((i + 1) * yearInDays / (appointmentCount + 1)).round();
|
|
final randomOffset = random.nextInt(21) - 10; // +/- 10 days
|
|
final dayOffset = targetDay + randomOffset;
|
|
|
|
final appointmentDate = startDate.add(Duration(days: dayOffset));
|
|
appointmentDates.add(appointmentDate);
|
|
}
|
|
|
|
// Sort dates and ensure no duplicates
|
|
appointmentDates.sort((a, b) => a.compareTo(b));
|
|
final uniqueDates = <DateTime>[];
|
|
DateTime? lastDate;
|
|
|
|
for (final date in appointmentDates) {
|
|
if (lastDate == null || date.difference(lastDate).inDays > 30) {
|
|
// Ensure at least 30 days between appointments
|
|
uniqueDates.add(date);
|
|
lastDate = date;
|
|
}
|
|
}
|
|
|
|
// Create and insert appointment records
|
|
for (final date in uniqueDates) {
|
|
final appointment = _generateAppointmentEntry(date, random);
|
|
await databaseService.insertBloodwork(appointment);
|
|
}
|
|
|
|
debugPrint('Generated ${uniqueDates.length} doctor appointment records');
|
|
}
|
|
|
|
/// Generate a surgery appointment within the year
|
|
Future<void> _generateSurgeryAppointment(
|
|
DateTime startDate, DateTime endDate, Random random) async {
|
|
// Place surgery in the middle third of the year (not too recent, not too far back)
|
|
final yearInDays = 364;
|
|
final targetDayStart = yearInDays ~/ 3;
|
|
final targetDayEnd = (yearInDays * 2) ~/ 3;
|
|
|
|
final dayOffset =
|
|
random.nextInt(targetDayEnd - targetDayStart) + targetDayStart;
|
|
final surgeryDate = startDate.add(Duration(days: dayOffset));
|
|
|
|
final surgery = _generateSurgeryEntry(surgeryDate, random);
|
|
await databaseService.insertBloodwork(surgery);
|
|
|
|
debugPrint('Generated surgery record for ${surgeryDate.toIso8601String()}');
|
|
}
|
|
|
|
/// Generate a bloodwork lab entry
|
|
Bloodwork _generateBloodworkEntry(DateTime date, Random random) {
|
|
// Always include the four required hormones
|
|
final hormoneReadings = <HormoneReading>[
|
|
_generateHormoneReading('Estradiol', random),
|
|
_generateHormoneReading('Testosterone', random),
|
|
_generateHormoneReading('Progesterone', random),
|
|
_generateHormoneReading('Prolactin', random),
|
|
];
|
|
|
|
// Add 0-6 additional random hormones
|
|
final additionalCount = random.nextInt(7); // 0-6
|
|
final allHormoneTypes = HormoneTypes.getHormoneTypes();
|
|
final requiredTypes = {
|
|
'Estradiol',
|
|
'Testosterone',
|
|
'Progesterone',
|
|
'Prolactin'
|
|
};
|
|
final availableTypes =
|
|
allHormoneTypes.where((type) => !requiredTypes.contains(type)).toList();
|
|
|
|
// Shuffle and take random selection
|
|
availableTypes.shuffle(random);
|
|
for (int i = 0; i < additionalCount && i < availableTypes.length; i++) {
|
|
hormoneReadings.add(_generateHormoneReading(availableTypes[i], random));
|
|
}
|
|
|
|
// Random time
|
|
final timeHour = random.nextInt(8) + 8; // Between a8am and 4pm
|
|
final timeMinute = random.nextInt(4) * 15; // 0, 15, 30, or 45 minutes
|
|
final dateTime = DateTime(
|
|
date.year,
|
|
date.month,
|
|
date.day,
|
|
timeHour,
|
|
timeMinute,
|
|
);
|
|
|
|
// Random location
|
|
final locations = [
|
|
'Main Hospital Lab',
|
|
'Downtown Clinic',
|
|
'Westside Medical Center',
|
|
'University Health Services',
|
|
'Eastside Lab',
|
|
];
|
|
final location = locations[random.nextInt(locations.length)];
|
|
|
|
// Random doctor
|
|
final doctors = [
|
|
'Dr. Martinez',
|
|
'Dr. Johnson',
|
|
'Dr. Williams',
|
|
'Dr. Chen',
|
|
'Dr. Taylor',
|
|
];
|
|
final doctor = doctors[random.nextInt(doctors.length)];
|
|
|
|
// Random notes
|
|
final notesOptions = [
|
|
'Routine bloodwork',
|
|
'Follow-up lab work',
|
|
'Quarterly hormone check',
|
|
'Regular monitoring',
|
|
'Medication adjustment labs',
|
|
null, // Sometimes no notes
|
|
];
|
|
final notes = notesOptions[random.nextInt(notesOptions.length)];
|
|
|
|
return Bloodwork(
|
|
date: dateTime,
|
|
appointmentType: AppointmentType.bloodwork,
|
|
hormoneReadings: hormoneReadings,
|
|
location: location,
|
|
doctor: doctor,
|
|
notes: notes,
|
|
);
|
|
}
|
|
|
|
/// Generate a doctor appointment entry
|
|
Bloodwork _generateAppointmentEntry(DateTime date, Random random) {
|
|
// Random time
|
|
final timeHour = random.nextInt(8) + 9; // Between 9am and 5pm
|
|
final timeMinute = random.nextInt(4) * 15; // 0, 15, 30, or 45 minutes
|
|
final dateTime = DateTime(
|
|
date.year,
|
|
date.month,
|
|
date.day,
|
|
timeHour,
|
|
timeMinute,
|
|
);
|
|
|
|
// Random location
|
|
final locations = [
|
|
'Primary Care Clinic',
|
|
'Endocrinology Department',
|
|
'Specialty Clinic',
|
|
'Medical Center',
|
|
'Healthcare Associates',
|
|
];
|
|
final location = locations[random.nextInt(locations.length)];
|
|
|
|
// Random doctor
|
|
final doctors = [
|
|
'Dr. Martinez',
|
|
'Dr. Johnson',
|
|
'Dr. Williams',
|
|
'Dr. Chen',
|
|
'Dr. Taylor',
|
|
];
|
|
final doctor = doctors[random.nextInt(doctors.length)];
|
|
|
|
// Random notes
|
|
final notesOptions = [
|
|
'Regular checkup',
|
|
'Follow-up appointment',
|
|
'Medication review',
|
|
'Consultation',
|
|
'Care planning',
|
|
'Treatment discussion',
|
|
null, // Sometimes no notes
|
|
];
|
|
final notes = notesOptions[random.nextInt(notesOptions.length)];
|
|
|
|
return Bloodwork(
|
|
date: dateTime,
|
|
appointmentType: AppointmentType.appointment,
|
|
hormoneReadings: [], // No hormone readings for appointments
|
|
location: location,
|
|
doctor: doctor,
|
|
notes: notes,
|
|
);
|
|
}
|
|
|
|
/// Generate a surgery entry
|
|
Bloodwork _generateSurgeryEntry(DateTime date, Random random) {
|
|
// Random time (surgeries often early)
|
|
final timeHour = random.nextInt(5) + 7; // Between 7am and 12pm
|
|
final timeMinute = random.nextInt(4) * 15; // 0, 15, 30, or 45 minutes
|
|
final dateTime = DateTime(
|
|
date.year,
|
|
date.month,
|
|
date.day,
|
|
timeHour,
|
|
timeMinute,
|
|
);
|
|
|
|
// Random location
|
|
final locations = [
|
|
'University Hospital',
|
|
'Memorial Surgery Center',
|
|
'Medical Center',
|
|
'Surgical Institute',
|
|
'Regional Hospital',
|
|
];
|
|
final location = locations[random.nextInt(locations.length)];
|
|
|
|
// Random doctor
|
|
final doctors = [
|
|
'Dr. Garcia',
|
|
'Dr. Smith',
|
|
'Dr. Patel',
|
|
'Dr. Wilson',
|
|
'Dr. Lee',
|
|
];
|
|
final doctor = doctors[random.nextInt(doctors.length)];
|
|
|
|
// Random notes about the surgery
|
|
final notesOptions = [
|
|
'Scheduled procedure',
|
|
'Outpatient surgery',
|
|
'Routine operation',
|
|
'Elective procedure',
|
|
'Standard intervention',
|
|
];
|
|
final notes = notesOptions[random.nextInt(notesOptions.length)];
|
|
|
|
return Bloodwork(
|
|
date: dateTime,
|
|
appointmentType: AppointmentType.surgery,
|
|
hormoneReadings: [], // No hormone readings for surgery
|
|
location: location,
|
|
doctor: doctor,
|
|
notes: notes,
|
|
);
|
|
}
|
|
|
|
/// Generate a random hormone reading
|
|
HormoneReading _generateHormoneReading(String hormoneName, Random random) {
|
|
// Get the default unit for this hormone
|
|
final unit = HormoneTypes.getDefaultUnit(hormoneName);
|
|
|
|
// Generate a reasonable value based on the hormone type
|
|
double value;
|
|
|
|
switch (hormoneName) {
|
|
case 'Estradiol':
|
|
// Typical range: 30-400 pg/mL
|
|
value = 30.0 + random.nextDouble() * 370.0;
|
|
break;
|
|
case 'Testosterone':
|
|
// Typical range: 15-70 ng/dL for women, 300-1000 ng/dL for men
|
|
value = 15.0 + random.nextDouble() * 500.0;
|
|
break;
|
|
case 'Progesterone':
|
|
// Typical range: 0.1-25 ng/mL
|
|
value = 0.1 + random.nextDouble() * 24.9;
|
|
break;
|
|
case 'Prolactin':
|
|
// Typical range: 3-30 ng/mL
|
|
value = 3.0 + random.nextDouble() * 27.0;
|
|
break;
|
|
case 'FSH':
|
|
// Typical range: 4-25 mIU/mL
|
|
value = 4.0 + random.nextDouble() * 21.0;
|
|
break;
|
|
case 'LH':
|
|
// Typical range: 2-20 mIU/mL
|
|
value = 2.0 + random.nextDouble() * 18.0;
|
|
break;
|
|
case 'TSH':
|
|
// Typical range: 0.4-4.0 μIU/mL
|
|
value = 0.4 + random.nextDouble() * 3.6;
|
|
break;
|
|
case 'Free T3':
|
|
// Typical range: 2.3-4.2 pg/mL
|
|
value = 2.3 + random.nextDouble() * 1.9;
|
|
break;
|
|
case 'Free T4':
|
|
// Typical range: 0.8-1.8 ng/dL
|
|
value = 0.8 + random.nextDouble() * 1.0;
|
|
break;
|
|
case 'SHBG':
|
|
// Typical range: 20-130 nmol/L
|
|
value = 20.0 + random.nextDouble() * 110.0;
|
|
break;
|
|
case 'DHT':
|
|
// Typical range: 24-65 ng/dL
|
|
value = 24.0 + random.nextDouble() * 41.0;
|
|
break;
|
|
default:
|
|
// Default range for other hormones
|
|
value = 10.0 + random.nextDouble() * 90.0;
|
|
}
|
|
|
|
// Round to 1 decimal place
|
|
value = (value * 10).round() / 10;
|
|
|
|
return HormoneReading(
|
|
name: hormoneName,
|
|
value: value,
|
|
unit: unit,
|
|
);
|
|
}
|
|
}
|