// SPDX-FileCopyrightText: © 2025 Nøkken.io // 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 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 _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 _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 = []; 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? 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 _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 = [ _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, ); } }