mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-12-08 06:02:34 +00:00
- 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.
471 lines
15 KiB
Dart
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],
|
|
);
|
|
}
|
|
}
|