Files
supplements/lib/services/database_helper.dart
Menno van Leeuwen 2aec59ec35 feat: Add settings provider for theme and time range management
- Implemented SettingsProvider to manage user preferences for theme options and time ranges for reminders.
- Added persistent reminder settings with configurable retry intervals and maximum attempts.
- Created UI for settings screen to allow users to customize their preferences.
- Integrated shared_preferences for persistent storage of user settings.

feat: Introduce Ingredient model

- Created Ingredient model to represent nutritional components with properties for id, name, amount, and unit.
- Added methods for serialization and deserialization of Ingredient objects.

feat: Develop Archived Supplements Screen

- Implemented ArchivedSupplementsScreen to display archived supplements with options to unarchive or delete.
- Added UI components for listing archived supplements and handling user interactions.

chore: Update dependencies in pubspec.yaml and pubspec.lock

- Updated shared_preferences dependency to the latest version.
- Removed flutter_datetime_picker_plus dependency and added file dependency.
- Updated Flutter SDK constraint to >=3.27.0.
2025-08-26 17:19:54 +02:00

471 lines
15 KiB
Dart

import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:path/path.dart';
import 'dart:io';
import 'dart:convert';
import '../models/supplement.dart';
import '../models/supplement_intake.dart';
class DatabaseHelper {
static const _databaseName = 'supplements.db';
static const _databaseVersion = 5; // Increment version for notification tracking
static const supplementsTable = 'supplements';
static const intakesTable = 'supplement_intakes';
static const notificationTrackingTable = 'notification_tracking';
DatabaseHelper._privateConstructor();
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
static Database? _database;
static bool _initialized = false;
static void _initializeDatabaseFactory() {
if (!_initialized) {
// Initialize for desktop platforms
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
_initialized = true;
}
}
Future<Database> get database async {
_initializeDatabaseFactory();
_database ??= await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
String path = join(await getDatabasesPath(), _databaseName);
return await openDatabase(
path,
version: _databaseVersion,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE $supplementsTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
brand TEXT,
ingredients TEXT NOT NULL DEFAULT '[]',
numberOfUnits INTEGER NOT NULL DEFAULT 1,
unitType TEXT NOT NULL DEFAULT 'units',
frequencyPerDay INTEGER NOT NULL,
reminderTimes TEXT NOT NULL,
notes TEXT,
createdAt TEXT NOT NULL,
isActive INTEGER NOT NULL DEFAULT 1
)
''');
await db.execute('''
CREATE TABLE $intakesTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
supplementId INTEGER NOT NULL,
takenAt TEXT NOT NULL,
dosageTaken REAL NOT NULL,
unitsTaken REAL NOT NULL DEFAULT 1,
notes TEXT,
FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id)
)
''');
await db.execute('''
CREATE TABLE $notificationTrackingTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
notificationId INTEGER NOT NULL UNIQUE,
supplementId INTEGER NOT NULL,
scheduledTime TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
retryCount INTEGER NOT NULL DEFAULT 0,
lastRetryTime TEXT,
createdAt TEXT NOT NULL,
FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id)
)
''');
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// First, add new columns
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN dosageAmount REAL DEFAULT 0');
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN numberOfUnits INTEGER DEFAULT 1');
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN unitType TEXT DEFAULT "units"');
await db.execute('ALTER TABLE $intakesTable ADD COLUMN unitsTaken REAL DEFAULT 1');
// Migrate existing data from old dosage column to new dosageAmount column
await db.execute('''
UPDATE $supplementsTable
SET dosageAmount = COALESCE(dosage, 0),
numberOfUnits = 1,
unitType = 'units'
WHERE dosageAmount = 0
''');
// Create new table with correct schema
await db.execute('''
CREATE TABLE ${supplementsTable}_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
brand TEXT,
dosageAmount REAL NOT NULL,
numberOfUnits INTEGER NOT NULL DEFAULT 1,
unit TEXT NOT NULL,
unitType TEXT NOT NULL DEFAULT 'units',
frequencyPerDay INTEGER NOT NULL,
reminderTimes TEXT NOT NULL,
notes TEXT,
createdAt TEXT NOT NULL,
isActive INTEGER NOT NULL DEFAULT 1
)
''');
// Copy data to new table
await db.execute('''
INSERT INTO ${supplementsTable}_new
(id, name, brand, dosageAmount, numberOfUnits, unit, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive)
SELECT id, name, NULL as brand, dosageAmount, numberOfUnits, unit, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive
FROM $supplementsTable
''');
// Drop old table and rename new table
await db.execute('DROP TABLE $supplementsTable');
await db.execute('ALTER TABLE ${supplementsTable}_new RENAME TO $supplementsTable');
}
if (oldVersion < 3) {
// Add brand column for version 3
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN brand TEXT');
}
if (oldVersion < 4) {
// Complete migration to new ingredient-based schema
// Add ingredients column and migrate old data
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN ingredients TEXT DEFAULT "[]"');
// Migrate existing supplements to use ingredients format
final supplements = await db.query(supplementsTable);
for (final supplement in supplements) {
final dosageAmount = supplement['dosageAmount'] as double?;
final unit = supplement['unit'] as String?;
final name = supplement['name'] as String;
if (dosageAmount != null && unit != null && dosageAmount > 0) {
// Create a single ingredient from the old dosage data
final ingredient = {
'name': name,
'amount': dosageAmount,
'unit': unit,
};
final ingredientsJson = jsonEncode([ingredient]);
await db.update(
supplementsTable,
{'ingredients': ingredientsJson},
where: 'id = ?',
whereArgs: [supplement['id']],
);
}
}
// Remove old columns
await db.execute('''
CREATE TABLE ${supplementsTable}_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
brand TEXT,
ingredients TEXT NOT NULL DEFAULT '[]',
numberOfUnits INTEGER NOT NULL DEFAULT 1,
unitType TEXT NOT NULL DEFAULT 'units',
frequencyPerDay INTEGER NOT NULL,
reminderTimes TEXT NOT NULL,
notes TEXT,
createdAt TEXT NOT NULL,
isActive INTEGER NOT NULL DEFAULT 1
)
''');
await db.execute('''
INSERT INTO ${supplementsTable}_new
(id, name, brand, ingredients, numberOfUnits, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive)
SELECT id, name, brand, ingredients, numberOfUnits, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive
FROM $supplementsTable
''');
await db.execute('DROP TABLE $supplementsTable');
await db.execute('ALTER TABLE ${supplementsTable}_new RENAME TO $supplementsTable');
}
if (oldVersion < 5) {
// Add notification tracking table
await db.execute('''
CREATE TABLE $notificationTrackingTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
notificationId INTEGER NOT NULL UNIQUE,
supplementId INTEGER NOT NULL,
scheduledTime TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
retryCount INTEGER NOT NULL DEFAULT 0,
lastRetryTime TEXT,
createdAt TEXT NOT NULL,
FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id)
)
''');
}
}
// Supplement CRUD operations
Future<int> insertSupplement(Supplement supplement) async {
Database db = await database;
return await db.insert(supplementsTable, supplement.toMap());
}
Future<List<Supplement>> getAllSupplements() async {
Database db = await database;
List<Map<String, dynamic>> maps = await db.query(
supplementsTable,
where: 'isActive = ?',
whereArgs: [1],
orderBy: 'name ASC',
);
return List.generate(maps.length, (i) => Supplement.fromMap(maps[i]));
}
Future<List<Supplement>> getArchivedSupplements() async {
Database db = await database;
List<Map<String, dynamic>> maps = await db.query(
supplementsTable,
where: 'isActive = ?',
whereArgs: [0],
orderBy: 'name ASC',
);
return List.generate(maps.length, (i) => Supplement.fromMap(maps[i]));
}
Future<void> archiveSupplement(int id) async {
Database db = await database;
await db.update(
supplementsTable,
{'isActive': 0},
where: 'id = ?',
whereArgs: [id],
);
}
Future<void> unarchiveSupplement(int id) async {
Database db = await database;
await db.update(
supplementsTable,
{'isActive': 1},
where: 'id = ?',
whereArgs: [id],
);
}
Future<Supplement?> getSupplement(int id) async {
Database db = await database;
List<Map<String, dynamic>> maps = await db.query(
supplementsTable,
where: 'id = ?',
whereArgs: [id],
);
if (maps.isNotEmpty) {
return Supplement.fromMap(maps.first);
}
return null;
}
Future<int> updateSupplement(Supplement supplement) async {
Database db = await database;
return await db.update(
supplementsTable,
supplement.toMap(),
where: 'id = ?',
whereArgs: [supplement.id],
);
}
Future<int> deleteSupplement(int id) async {
Database db = await database;
return await db.update(
supplementsTable,
{'isActive': 0},
where: 'id = ?',
whereArgs: [id],
);
}
// Supplement Intake CRUD operations
Future<int> insertIntake(SupplementIntake intake) async {
Database db = await database;
return await db.insert(intakesTable, intake.toMap());
}
Future<List<SupplementIntake>> getIntakesForDate(DateTime date) async {
Database db = await database;
String startDate = DateTime(date.year, date.month, date.day).toIso8601String();
String endDate = DateTime(date.year, date.month, date.day, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> maps = await db.query(
intakesTable,
where: 'takenAt >= ? AND takenAt <= ?',
whereArgs: [startDate, endDate],
orderBy: 'takenAt DESC',
);
return List.generate(maps.length, (i) => SupplementIntake.fromMap(maps[i]));
}
Future<List<SupplementIntake>> getIntakesForMonth(int year, int month) async {
Database db = await database;
String startDate = DateTime(year, month, 1).toIso8601String();
String endDate = DateTime(year, month + 1, 0, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> maps = await db.query(
intakesTable,
where: 'takenAt >= ? AND takenAt <= ?',
whereArgs: [startDate, endDate],
orderBy: 'takenAt DESC',
);
return List.generate(maps.length, (i) => SupplementIntake.fromMap(maps[i]));
}
Future<List<Map<String, dynamic>>> getIntakesWithSupplementsForDate(DateTime date) async {
Database db = await database;
String startDate = DateTime(date.year, date.month, date.day).toIso8601String();
String endDate = DateTime(date.year, date.month, date.day, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> result = await db.rawQuery('''
SELECT i.*,
i.supplementId as supplement_id,
s.name as supplementName,
s.unitType as supplementUnitType
FROM $intakesTable i
JOIN $supplementsTable s ON i.supplementId = s.id
WHERE i.takenAt >= ? AND i.takenAt <= ?
ORDER BY i.takenAt DESC
''', [startDate, endDate]);
return result;
}
Future<List<Map<String, dynamic>>> getIntakesWithSupplementsForMonth(int year, int month) async {
Database db = await database;
String startDate = DateTime(year, month, 1).toIso8601String();
String endDate = DateTime(year, month + 1, 0, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> result = await db.rawQuery('''
SELECT i.*,
i.supplementId as supplement_id,
s.name as supplementName,
s.unitType as supplementUnitType
FROM $intakesTable i
JOIN $supplementsTable s ON i.supplementId = s.id
WHERE i.takenAt >= ? AND i.takenAt <= ?
ORDER BY i.takenAt DESC
''', [startDate, endDate]);
return result;
}
Future<int> deleteIntake(int id) async {
Database db = await database;
return await db.delete(
intakesTable,
where: 'id = ?',
whereArgs: [id],
);
}
// Notification tracking methods
Future<int> trackNotification({
required int notificationId,
required int supplementId,
required DateTime scheduledTime,
}) async {
Database db = await database;
// Use INSERT OR REPLACE to handle both new and existing notifications
await db.rawInsert('''
INSERT OR REPLACE INTO $notificationTrackingTable
(notificationId, supplementId, scheduledTime, status, retryCount, lastRetryTime, createdAt)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', [
notificationId,
supplementId,
scheduledTime.toIso8601String(),
'pending',
0,
null,
DateTime.now().toIso8601String(),
]);
return notificationId;
}
Future<void> markNotificationTaken(int notificationId) async {
Database db = await database;
await db.update(
notificationTrackingTable,
{'status': 'taken'},
where: 'notificationId = ?',
whereArgs: [notificationId],
);
}
Future<void> incrementRetryCount(int notificationId) async {
Database db = await database;
await db.rawUpdate('''
UPDATE $notificationTrackingTable
SET retryCount = retryCount + 1,
lastRetryTime = ?,
status = 'retrying'
WHERE notificationId = ?
''', [DateTime.now().toIso8601String(), notificationId]);
}
Future<List<Map<String, dynamic>>> getPendingNotifications() async {
Database db = await database;
return await db.query(
notificationTrackingTable,
where: 'status IN (?, ?)',
whereArgs: ['pending', 'retrying'],
);
}
Future<void> markNotificationExpired(int notificationId) async {
Database db = await database;
await db.update(
notificationTrackingTable,
{'status': 'expired'},
where: 'notificationId = ?',
whereArgs: [notificationId],
);
}
Future<void> cleanupOldNotificationTracking() async {
Database db = await database;
// Remove tracking records older than 7 days
final cutoffDate = DateTime.now().subtract(const Duration(days: 7)).toIso8601String();
await db.delete(
notificationTrackingTable,
where: 'createdAt < ?',
whereArgs: [cutoffDate],
);
}
Future<void> clearNotificationTracking(int supplementId) async {
Database db = await database;
await db.delete(
notificationTrackingTable,
where: 'supplementId = ?',
whereArgs: [supplementId],
);
}
}