first commit - migrated from codeberg
This commit is contained in:
commit
5ead03e1f7
567 changed files with 102721 additions and 0 deletions
383
lib/tools/bloodwork_data_generator.dart
Normal file
383
lib/tools/bloodwork_data_generator.dart
Normal file
|
@ -0,0 +1,383 @@
|
|||
// 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,
|
||||
);
|
||||
}
|
||||
}
|
232
lib/tools/medication_data_generator.dart
Normal file
232
lib/tools/medication_data_generator.dart
Normal file
|
@ -0,0 +1,232 @@
|
|||
// SPDX-FileCopyrightText: © 2025 Nøkken.io <nokken.io@proton.me>
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
//
|
||||
// medication_data_generator.dart
|
||||
//
|
||||
import 'dart:math' show Random;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nokken/src/core/constants/date_constants.dart';
|
||||
import 'package:nokken/src/core/services/database/database_service.dart';
|
||||
import 'package:nokken/src/features/medication_tracker/models/medication.dart';
|
||||
import 'package:nokken/src/features/medication_tracker/models/medication_dose.dart';
|
||||
|
||||
/// A tool class to generate historical medication data
|
||||
class MedicationDataGenerator {
|
||||
final DatabaseService databaseService;
|
||||
|
||||
MedicationDataGenerator(this.databaseService);
|
||||
|
||||
/// Generate sample medications and adherence data for 365 days
|
||||
Future<void> generateMedicationData() 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 medication data from ${startDate.toIso8601String()} to ${endDate.toIso8601String()}');
|
||||
|
||||
// Check existing medications and add more if needed
|
||||
final existingMeds = await databaseService.getAllMedications();
|
||||
debugPrint('Found ${existingMeds.length} existing medications');
|
||||
|
||||
// We want to have at least 5 medications
|
||||
if (existingMeds.length < 5) {
|
||||
debugPrint('Creating additional sample medications...');
|
||||
final targetCount = 5 - existingMeds.length;
|
||||
|
||||
// Get sample medications and only use as many as needed
|
||||
final allSampleMeds = _createSampleMedications();
|
||||
final medsToAdd = allSampleMeds.take(targetCount).toList();
|
||||
|
||||
for (final med in medsToAdd) {
|
||||
await databaseService.insertMedication(med);
|
||||
}
|
||||
debugPrint('Created ${medsToAdd.length} additional medications.');
|
||||
}
|
||||
|
||||
// Get all medications
|
||||
final allMeds = await databaseService.getAllMedications();
|
||||
|
||||
// Generate taking records for each medication
|
||||
for (final med in allMeds) {
|
||||
debugPrint('Generating adherence data for ${med.name}...');
|
||||
|
||||
// Set adherence rate based on medication type
|
||||
double adherenceRate;
|
||||
if (med.medicationType == MedicationType.injection ||
|
||||
med.medicationType == MedicationType.patch) {
|
||||
// Higher adherence for injections and patches (96-98%)
|
||||
adherenceRate = 0.96 + (random.nextDouble() * 0.02);
|
||||
} else {
|
||||
// Lower adherence for pills and topical (95-97%)
|
||||
adherenceRate = 0.95 + (random.nextDouble() * 0.02);
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'Using adherence rate of ${(adherenceRate * 100).toStringAsFixed(1)}% for ${med.name}');
|
||||
|
||||
// Generate records for each day
|
||||
await _generateMedicationRecords(
|
||||
med, startDate, endDate, adherenceRate, random);
|
||||
}
|
||||
|
||||
debugPrint('Successfully generated medication adherence data.');
|
||||
}
|
||||
|
||||
/// Create 5 sample medications of different types
|
||||
List<Medication> _createSampleMedications() {
|
||||
final now = DateTime.now();
|
||||
final allDays = Set<String>.from(DateConstants.dayMap.values);
|
||||
|
||||
// Common morning and evening times
|
||||
final morning = DateTime(now.year, now.month, now.day, 8, 0);
|
||||
final evening = DateTime(now.year, now.month, now.day, 20, 0);
|
||||
final noon = DateTime(now.year, now.month, now.day, 12, 0);
|
||||
|
||||
return [
|
||||
// Pill 1: Daily antidepressant in the morning
|
||||
Medication(
|
||||
name: 'Sertraline',
|
||||
dosage: '50mg',
|
||||
startDate: DateTime.now().subtract(const Duration(days: 400)),
|
||||
frequency: 1,
|
||||
timeOfDay: [morning],
|
||||
daysOfWeek: allDays,
|
||||
currentQuantity: 28,
|
||||
refillThreshold: 7,
|
||||
medicationType: MedicationType.oral,
|
||||
oralSubtype: OralSubtype.tablets,
|
||||
notes: 'SSRI antidepressant',
|
||||
doctor: 'Dr. Smith',
|
||||
pharmacy: 'Local Pharmacy',
|
||||
),
|
||||
|
||||
// Pill 2: Multivitamin taken once daily
|
||||
Medication(
|
||||
name: 'Multivitamin',
|
||||
dosage: '1 tablet',
|
||||
startDate: DateTime.now().subtract(const Duration(days: 380)),
|
||||
frequency: 1,
|
||||
timeOfDay: [noon],
|
||||
daysOfWeek: allDays,
|
||||
currentQuantity: 60,
|
||||
refillThreshold: 10,
|
||||
medicationType: MedicationType.oral,
|
||||
oralSubtype: OralSubtype.tablets,
|
||||
),
|
||||
|
||||
// Topical: Gel applied twice daily
|
||||
Medication(
|
||||
name: 'Topical Treatment',
|
||||
dosage: 'Thin layer',
|
||||
startDate: DateTime.now().subtract(const Duration(days: 390)),
|
||||
frequency: 2,
|
||||
timeOfDay: [morning, evening],
|
||||
daysOfWeek: allDays,
|
||||
currentQuantity: 1,
|
||||
refillThreshold: 1,
|
||||
medicationType: MedicationType.topical,
|
||||
topicalSubtype: TopicalSubtype.gel,
|
||||
),
|
||||
|
||||
// Patch: Applied weekly
|
||||
Medication(
|
||||
name: 'Hormone Patch',
|
||||
dosage: '1 patch',
|
||||
startDate: DateTime.now().subtract(const Duration(days: 395)),
|
||||
frequency: 1,
|
||||
timeOfDay: [morning],
|
||||
daysOfWeek: {'Su'}, // Applied on Sundays
|
||||
currentQuantity: 4,
|
||||
refillThreshold: 2,
|
||||
medicationType: MedicationType.patch,
|
||||
),
|
||||
|
||||
// Injection: Intramuscular, biweekly
|
||||
Medication(
|
||||
name: 'Hormone Injection',
|
||||
dosage: '0.5ml',
|
||||
startDate: DateTime.now().subtract(const Duration(days: 392)),
|
||||
frequency: 1,
|
||||
timeOfDay: [evening],
|
||||
daysOfWeek: {'F'}, // Injected on Fridays
|
||||
currentQuantity: 2,
|
||||
refillThreshold: 1,
|
||||
medicationType: MedicationType.injection,
|
||||
injectionDetails: InjectionDetails(
|
||||
drawingNeedleType: '18G 1.5"',
|
||||
drawingNeedleCount: 8,
|
||||
drawingNeedleRefills: 2,
|
||||
injectingNeedleType: '23G 1"',
|
||||
injectingNeedleCount: 8,
|
||||
injectingNeedleRefills: 2,
|
||||
syringeType: '3ml Luer Lock',
|
||||
syringeCount: 8,
|
||||
syringeRefills: 2,
|
||||
injectionSiteNotes: 'Rotate between thighs and glutes',
|
||||
frequency: InjectionFrequency.biweekly,
|
||||
subtype: InjectionSubtype.intramuscular,
|
||||
siteRotation: InjectionSiteRotation(
|
||||
sites: [
|
||||
InjectionSite(siteNumber: 1, bodyArea: InjectionBodyArea.thigh),
|
||||
InjectionSite(siteNumber: 2, bodyArea: InjectionBodyArea.thigh),
|
||||
],
|
||||
currentSiteIndex: 0,
|
||||
),
|
||||
),
|
||||
doctor: 'Dr. Johnson',
|
||||
pharmacy: 'Specialty Pharmacy',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Generate medication taking records based on specified adherence patterns
|
||||
Future<void> _generateMedicationRecords(
|
||||
Medication medication,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
double adherenceRate,
|
||||
Random random) async {
|
||||
int recordCount = 0;
|
||||
|
||||
// For each day in the range
|
||||
for (int i = 0; i <= 364; i++) {
|
||||
final date = startDate.add(Duration(days: i));
|
||||
|
||||
// Check if this medication is scheduled for this day
|
||||
if (medication.isDueOnDate(date)) {
|
||||
// For each time slot
|
||||
for (int timeIndex = 0; timeIndex < medication.frequency; timeIndex++) {
|
||||
final timeSlot = timeIndex < medication.timeOfDay.length
|
||||
? medication.timeOfDay[timeIndex].hour.toString()
|
||||
: '$timeIndex';
|
||||
|
||||
// Create a proper MedicationDose object
|
||||
final dose = MedicationDose(
|
||||
medicationId: medication.id,
|
||||
date: date,
|
||||
timeSlot: timeSlot,
|
||||
);
|
||||
|
||||
// Generate a custom key from the dose plus a unique identifier
|
||||
final customKey = '${dose.toKey()}-historical-$i';
|
||||
|
||||
// Determine if the dose was taken based on adherence rate
|
||||
final wasTaken = random.nextDouble() <= adherenceRate;
|
||||
|
||||
// Save the taking record
|
||||
await databaseService.setMedicationTakenWithCustomKey(
|
||||
medication.id, date, timeSlot, wasTaken, customKey);
|
||||
|
||||
if (wasTaken) {
|
||||
recordCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('Generated $recordCount taking records for ${medication.name}');
|
||||
}
|
||||
}
|
226
lib/tools/mood_data_generator.dart
Normal file
226
lib/tools/mood_data_generator.dart
Normal file
|
@ -0,0 +1,226 @@
|
|||
// SPDX-FileCopyrightText: © 2025 Nøkken.io <nokken.io@proton.me>
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
//
|
||||
// mood_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/core/services/database/database_service_mood.dart';
|
||||
import 'package:nokken/src/features/mood_tracker/models/mood_entry.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// A tool class to generate historical mood data
|
||||
class MoodDataGenerator {
|
||||
final DatabaseService databaseService;
|
||||
|
||||
MoodDataGenerator(this.databaseService);
|
||||
|
||||
/// Generate 365 days of mood entries
|
||||
Future<void> generateMoodEntries() 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 mood entries from ${startDate.toIso8601String()} to ${endDate.toIso8601String()}');
|
||||
|
||||
// Generate an entry for each day
|
||||
for (int i = 0; i <= 364; i++) {
|
||||
final date = startDate.add(Duration(days: i));
|
||||
|
||||
// Generate random mood entry
|
||||
final entry = _generateRandomMoodEntry(date, random);
|
||||
|
||||
// Insert into database
|
||||
await databaseService.insertMoodEntry(entry);
|
||||
|
||||
// Print progress
|
||||
if (i % 30 == 0) {
|
||||
debugPrint('Generated ${i + 1} entries...');
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('Successfully generated 365 mood entries.');
|
||||
}
|
||||
|
||||
/// Generate a random but realistic mood entry for the given date
|
||||
MoodEntry _generateRandomMoodEntry(DateTime date, Random random) {
|
||||
// Generate a unique ID
|
||||
final id = const Uuid().v4();
|
||||
|
||||
// Generate a base mood index with some weekly and seasonal patterns
|
||||
int baseMoodModifier = 0;
|
||||
|
||||
// Weekday effects: people often feel better on weekends, worse on Mondays
|
||||
if (date.weekday == DateTime.saturday || date.weekday == DateTime.sunday) {
|
||||
baseMoodModifier =
|
||||
random.nextDouble() < 0.7 ? 1 : 0; // Better mood on weekends
|
||||
} else if (date.weekday == DateTime.monday) {
|
||||
baseMoodModifier =
|
||||
random.nextDouble() < 0.6 ? -1 : 0; // Worse mood on Mondays
|
||||
}
|
||||
|
||||
// Seasonal effects: worse in winter, better in summer
|
||||
final month = date.month;
|
||||
int seasonalModifier = 0;
|
||||
if (month >= 11 || month <= 2) {
|
||||
// Winter months
|
||||
seasonalModifier = random.nextDouble() < 0.6 ? -1 : 0;
|
||||
} else if (month >= 5 && month <= 8) {
|
||||
// Summer months
|
||||
seasonalModifier = random.nextDouble() < 0.6 ? 1 : 0;
|
||||
}
|
||||
|
||||
// Calculate final mood index with modifiers and randomness
|
||||
// Assuming mood from terrible (0) to excellent (4)
|
||||
int moodIndex =
|
||||
2 + baseMoodModifier + seasonalModifier; // Start from neutral (2)
|
||||
moodIndex += (random.nextInt(3) - 1); // Add -1, 0, or 1 for randomness
|
||||
moodIndex = moodIndex.clamp(0, 4); // Ensure within valid range
|
||||
|
||||
// Convert index to actual MoodRating enum
|
||||
final mood = MoodRating.values[moodIndex];
|
||||
|
||||
// Generate 1-6 random emotions
|
||||
final emotionCount = random.nextInt(6) + 1;
|
||||
final emotions = <Emotion>{};
|
||||
while (emotions.length < emotionCount &&
|
||||
emotions.length < Emotion.values.length) {
|
||||
final emotionIndex = random.nextInt(Emotion.values.length);
|
||||
emotions.add(Emotion.values[emotionIndex]);
|
||||
}
|
||||
|
||||
// Decide which health metrics to include (3 to all)
|
||||
final healthMetricsCount = random.nextInt(5) + 3; // 3 to 7 metrics
|
||||
final healthMetricsToInclude = List.generate(7, (index) => index)
|
||||
..shuffle(random);
|
||||
final selectedHealthMetrics =
|
||||
healthMetricsToInclude.take(healthMetricsCount).toList();
|
||||
|
||||
// Helper function to generate a correlated index
|
||||
int correlatedIndex(
|
||||
int baseIndex, double correlationStrength, int maxIndex) {
|
||||
if (random.nextDouble() < correlationStrength) {
|
||||
// Correlated value
|
||||
final variation = random.nextInt(3) - 1; // -1, 0, or 1
|
||||
return (baseIndex + variation).clamp(0, maxIndex - 1);
|
||||
} else {
|
||||
// Random value
|
||||
return random.nextInt(maxIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate health metrics
|
||||
SleepQuality? sleepQuality;
|
||||
EnergyLevel? energyLevel;
|
||||
LibidoLevel? libidoLevel;
|
||||
AppetiteLevel? appetiteLevel;
|
||||
FocusLevel? focusLevel;
|
||||
DysphoriaLevel? dysphoriaLevel;
|
||||
ExerciseLevel? exerciseLevel;
|
||||
|
||||
// Sleep quality - somewhat correlated with mood
|
||||
if (selectedHealthMetrics.contains(0)) {
|
||||
final sleepIndex =
|
||||
correlatedIndex(moodIndex, 0.7, SleepQuality.values.length);
|
||||
sleepQuality = SleepQuality.values[sleepIndex];
|
||||
}
|
||||
|
||||
// Energy level - often follows sleep quality if available
|
||||
if (selectedHealthMetrics.contains(1)) {
|
||||
int energyIndex;
|
||||
if (sleepQuality != null && random.nextDouble() < 0.8) {
|
||||
// Correlate with sleep
|
||||
energyIndex = correlatedIndex(SleepQuality.values.indexOf(sleepQuality),
|
||||
0.8, EnergyLevel.values.length);
|
||||
} else {
|
||||
// Correlate with mood
|
||||
energyIndex =
|
||||
correlatedIndex(moodIndex, 0.6, EnergyLevel.values.length);
|
||||
}
|
||||
energyLevel = EnergyLevel.values[energyIndex];
|
||||
}
|
||||
|
||||
// Libido level - moderately correlated with mood
|
||||
if (selectedHealthMetrics.contains(2)) {
|
||||
final libidoIndex =
|
||||
correlatedIndex(moodIndex, 0.5, LibidoLevel.values.length);
|
||||
libidoLevel = LibidoLevel.values[libidoIndex];
|
||||
}
|
||||
|
||||
// Appetite level - less correlated with mood
|
||||
if (selectedHealthMetrics.contains(3)) {
|
||||
final appetiteIndex =
|
||||
correlatedIndex(moodIndex, 0.4, AppetiteLevel.values.length);
|
||||
appetiteLevel = AppetiteLevel.values[appetiteIndex];
|
||||
}
|
||||
|
||||
// Focus level - often correlates with sleep and energy
|
||||
if (selectedHealthMetrics.contains(4)) {
|
||||
int focusIndex;
|
||||
if (sleepQuality != null &&
|
||||
energyLevel != null &&
|
||||
random.nextDouble() < 0.7) {
|
||||
// Derive from average of sleep and energy
|
||||
final avgIndex = (SleepQuality.values.indexOf(sleepQuality) +
|
||||
EnergyLevel.values.indexOf(energyLevel)) ~/
|
||||
2;
|
||||
focusIndex = correlatedIndex(avgIndex, 0.8, FocusLevel.values.length);
|
||||
} else {
|
||||
// Correlate with mood
|
||||
focusIndex = correlatedIndex(moodIndex, 0.6, FocusLevel.values.length);
|
||||
}
|
||||
focusLevel = FocusLevel.values[focusIndex];
|
||||
}
|
||||
|
||||
// Dysphoria level - often inversely correlates with mood
|
||||
if (selectedHealthMetrics.contains(5)) {
|
||||
// Invert the mood index for dysphoria correlation (higher mood = lower dysphoria)
|
||||
final invertedMood = 4 - moodIndex; // Assuming 5 mood levels (0-4)
|
||||
final dysphoriaIndex =
|
||||
correlatedIndex(invertedMood, 0.75, DysphoriaLevel.values.length);
|
||||
dysphoriaLevel = DysphoriaLevel.values[dysphoriaIndex];
|
||||
}
|
||||
|
||||
// Exercise level - less strongly correlated with other metrics
|
||||
if (selectedHealthMetrics.contains(6)) {
|
||||
final exerciseIndex = random.nextInt(ExerciseLevel.values.length);
|
||||
exerciseLevel = ExerciseLevel.values[exerciseIndex];
|
||||
}
|
||||
|
||||
// Generate optional notes (30% chance)
|
||||
String? notes;
|
||||
if (random.nextDouble() < 0.3) {
|
||||
final noteTemplates = [
|
||||
"Feeling ${moodIndex > 2 ? 'pretty good' : 'a bit down'} today.",
|
||||
"Today was ${moodIndex > 3 ? 'excellent' : moodIndex > 2 ? 'decent' : 'challenging'}.",
|
||||
"${moodIndex > 3 ? 'Great' : moodIndex > 2 ? 'Good' : 'Tough'} day overall.",
|
||||
"Noticed ${sleepQuality != null ? (SleepQuality.values.indexOf(sleepQuality) > 2 ? 'good sleep' : 'poor sleep') : 'fluctuating energy'} today.",
|
||||
"Mood tracker note for ${date.day}/${date.month}/${date.year}.",
|
||||
"${energyLevel != null && EnergyLevel.values.indexOf(energyLevel) > 2 ? 'High energy' : 'Low energy'} but ${moodIndex > 2 ? 'good spirits' : 'feeling down'}.",
|
||||
"Trying to stay ${moodIndex < 2 ? 'positive despite challenges' : 'grateful for the good things'}.",
|
||||
];
|
||||
|
||||
notes = noteTemplates[random.nextInt(noteTemplates.length)];
|
||||
}
|
||||
|
||||
// Create and return the mood entry
|
||||
return MoodEntry(
|
||||
id: id,
|
||||
date: date,
|
||||
mood: mood,
|
||||
emotions: emotions,
|
||||
notes: notes,
|
||||
sleepQuality: sleepQuality,
|
||||
energyLevel: energyLevel,
|
||||
libidoLevel: libidoLevel,
|
||||
appetiteLevel: appetiteLevel,
|
||||
focusLevel: focusLevel,
|
||||
dysphoriaLevel: dysphoriaLevel,
|
||||
exerciseLevel: exerciseLevel,
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue