mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 18:29:12 +02:00
adds syncing
This commit is contained in:
@@ -1,18 +1,22 @@
|
||||
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 'dart:io';
|
||||
|
||||
import 'package:path/path.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
|
||||
import '../models/supplement.dart';
|
||||
import '../models/supplement_intake.dart';
|
||||
import '../models/sync_enums.dart';
|
||||
|
||||
class DatabaseHelper {
|
||||
static const _databaseName = 'supplements.db';
|
||||
static const _databaseVersion = 5; // Increment version for notification tracking
|
||||
static const _databaseVersion = 6; // Increment version for sync support
|
||||
|
||||
static const supplementsTable = 'supplements';
|
||||
static const intakesTable = 'supplement_intakes';
|
||||
static const notificationTrackingTable = 'notification_tracking';
|
||||
static const syncMetadataTable = 'sync_metadata';
|
||||
static const deviceInfoTable = 'device_info';
|
||||
|
||||
DatabaseHelper._privateConstructor();
|
||||
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
|
||||
@@ -60,7 +64,11 @@ class DatabaseHelper {
|
||||
reminderTimes TEXT NOT NULL,
|
||||
notes TEXT,
|
||||
createdAt TEXT NOT NULL,
|
||||
isActive INTEGER NOT NULL DEFAULT 1
|
||||
isActive INTEGER NOT NULL DEFAULT 1,
|
||||
syncId TEXT NOT NULL UNIQUE,
|
||||
lastModified TEXT NOT NULL,
|
||||
syncStatus TEXT NOT NULL DEFAULT 'pending',
|
||||
isDeleted INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
@@ -72,6 +80,10 @@ class DatabaseHelper {
|
||||
dosageTaken REAL NOT NULL,
|
||||
unitsTaken REAL NOT NULL DEFAULT 1,
|
||||
notes TEXT,
|
||||
syncId TEXT NOT NULL UNIQUE,
|
||||
lastModified TEXT NOT NULL,
|
||||
syncStatus TEXT NOT NULL DEFAULT 'pending',
|
||||
isDeleted INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id)
|
||||
)
|
||||
''');
|
||||
@@ -89,6 +101,27 @@ class DatabaseHelper {
|
||||
FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id)
|
||||
)
|
||||
''');
|
||||
|
||||
// Sync metadata table for tracking sync operations
|
||||
await db.execute('''
|
||||
CREATE TABLE $syncMetadataTable (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
value TEXT NOT NULL,
|
||||
lastUpdated TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
// Device info table for conflict resolution
|
||||
await db.execute('''
|
||||
CREATE TABLE $deviceInfoTable (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
deviceId TEXT NOT NULL UNIQUE,
|
||||
deviceName TEXT NOT NULL,
|
||||
lastSyncTime TEXT,
|
||||
createdAt TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
||||
@@ -98,16 +131,16 @@ class DatabaseHelper {
|
||||
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),
|
||||
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 (
|
||||
@@ -125,37 +158,37 @@ class DatabaseHelper {
|
||||
isActive INTEGER NOT NULL DEFAULT 1
|
||||
)
|
||||
''');
|
||||
|
||||
|
||||
// Copy data to new table
|
||||
await db.execute('''
|
||||
INSERT INTO ${supplementsTable}_new
|
||||
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 = {
|
||||
@@ -164,7 +197,7 @@ class DatabaseHelper {
|
||||
'unit': unit,
|
||||
};
|
||||
final ingredientsJson = jsonEncode([ingredient]);
|
||||
|
||||
|
||||
await db.update(
|
||||
supplementsTable,
|
||||
{'ingredients': ingredientsJson},
|
||||
@@ -173,7 +206,7 @@ class DatabaseHelper {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Remove old columns
|
||||
await db.execute('''
|
||||
CREATE TABLE ${supplementsTable}_new (
|
||||
@@ -190,18 +223,18 @@ class DatabaseHelper {
|
||||
isActive INTEGER NOT NULL DEFAULT 1
|
||||
)
|
||||
''');
|
||||
|
||||
|
||||
await db.execute('''
|
||||
INSERT INTO ${supplementsTable}_new
|
||||
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('''
|
||||
@@ -218,6 +251,77 @@ class DatabaseHelper {
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
if (oldVersion < 6) {
|
||||
// Add sync columns to existing tables
|
||||
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN syncId TEXT');
|
||||
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN lastModified TEXT');
|
||||
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN syncStatus TEXT DEFAULT "pending"');
|
||||
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN isDeleted INTEGER DEFAULT 0');
|
||||
|
||||
await db.execute('ALTER TABLE $intakesTable ADD COLUMN syncId TEXT');
|
||||
await db.execute('ALTER TABLE $intakesTable ADD COLUMN lastModified TEXT');
|
||||
await db.execute('ALTER TABLE $intakesTable ADD COLUMN syncStatus TEXT DEFAULT "pending"');
|
||||
await db.execute('ALTER TABLE $intakesTable ADD COLUMN isDeleted INTEGER DEFAULT 0');
|
||||
|
||||
// Generate sync IDs and timestamps for existing records
|
||||
final supplements = await db.query(supplementsTable);
|
||||
for (final supplement in supplements) {
|
||||
if (supplement['syncId'] == null) {
|
||||
final now = DateTime.now().toIso8601String();
|
||||
await db.update(
|
||||
supplementsTable,
|
||||
{
|
||||
'syncId': 'sync-${supplement['id']}-${DateTime.now().millisecondsSinceEpoch}',
|
||||
'lastModified': now,
|
||||
'syncStatus': 'pending',
|
||||
'isDeleted': 0,
|
||||
},
|
||||
where: 'id = ?',
|
||||
whereArgs: [supplement['id']],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final intakes = await db.query(intakesTable);
|
||||
for (final intake in intakes) {
|
||||
if (intake['syncId'] == null) {
|
||||
final now = DateTime.now().toIso8601String();
|
||||
await db.update(
|
||||
intakesTable,
|
||||
{
|
||||
'syncId': 'sync-${intake['id']}-${DateTime.now().millisecondsSinceEpoch}',
|
||||
'lastModified': now,
|
||||
'syncStatus': 'pending',
|
||||
'isDeleted': 0,
|
||||
},
|
||||
where: 'id = ?',
|
||||
whereArgs: [intake['id']],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create sync metadata table
|
||||
await db.execute('''
|
||||
CREATE TABLE $syncMetadataTable (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
value TEXT NOT NULL,
|
||||
lastUpdated TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
// Create device info table
|
||||
await db.execute('''
|
||||
CREATE TABLE $deviceInfoTable (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
deviceId TEXT NOT NULL UNIQUE,
|
||||
deviceName TEXT NOT NULL,
|
||||
lastSyncTime TEXT,
|
||||
createdAt TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
}
|
||||
}
|
||||
|
||||
// Supplement CRUD operations
|
||||
@@ -230,8 +334,8 @@ class DatabaseHelper {
|
||||
Database db = await database;
|
||||
List<Map<String, dynamic>> maps = await db.query(
|
||||
supplementsTable,
|
||||
where: 'isActive = ?',
|
||||
whereArgs: [1],
|
||||
where: 'isActive = ? AND isDeleted = ?',
|
||||
whereArgs: [1, 0],
|
||||
orderBy: 'name ASC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Supplement.fromMap(maps[i]));
|
||||
@@ -241,8 +345,8 @@ class DatabaseHelper {
|
||||
Database db = await database;
|
||||
List<Map<String, dynamic>> maps = await db.query(
|
||||
supplementsTable,
|
||||
where: 'isActive = ?',
|
||||
whereArgs: [0],
|
||||
where: 'isActive = ? AND isDeleted = ?',
|
||||
whereArgs: [0, 0],
|
||||
orderBy: 'name ASC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Supplement.fromMap(maps[i]));
|
||||
@@ -311,11 +415,11 @@ class DatabaseHelper {
|
||||
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],
|
||||
where: 'takenAt >= ? AND takenAt <= ? AND isDeleted = ?',
|
||||
whereArgs: [startDate, endDate, 0],
|
||||
orderBy: 'takenAt DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => SupplementIntake.fromMap(maps[i]));
|
||||
@@ -325,11 +429,11 @@ class DatabaseHelper {
|
||||
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],
|
||||
where: 'takenAt >= ? AND takenAt <= ? AND isDeleted = ?',
|
||||
whereArgs: [startDate, endDate, 0],
|
||||
orderBy: 'takenAt DESC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => SupplementIntake.fromMap(maps[i]));
|
||||
@@ -339,18 +443,18 @@ class DatabaseHelper {
|
||||
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.*,
|
||||
SELECT i.*,
|
||||
i.supplementId as supplement_id,
|
||||
s.name as supplementName,
|
||||
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 <= ?
|
||||
WHERE i.takenAt >= ? AND i.takenAt <= ? AND i.isDeleted = ?
|
||||
ORDER BY i.takenAt DESC
|
||||
''', [startDate, endDate]);
|
||||
|
||||
''', [startDate, endDate, 0]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -358,18 +462,18 @@ class DatabaseHelper {
|
||||
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.*,
|
||||
SELECT i.*,
|
||||
i.supplementId as supplement_id,
|
||||
s.name as supplementName,
|
||||
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 <= ?
|
||||
WHERE i.takenAt >= ? AND i.takenAt <= ? AND i.isDeleted = ?
|
||||
ORDER BY i.takenAt DESC
|
||||
''', [startDate, endDate]);
|
||||
|
||||
''', [startDate, endDate, 0]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -389,11 +493,11 @@ class DatabaseHelper {
|
||||
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)
|
||||
INSERT OR REPLACE INTO $notificationTrackingTable
|
||||
(notificationId, supplementId, scheduledTime, status, retryCount, lastRetryTime, createdAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
''', [
|
||||
notificationId,
|
||||
@@ -404,7 +508,7 @@ class DatabaseHelper {
|
||||
null,
|
||||
DateTime.now().toIso8601String(),
|
||||
]);
|
||||
|
||||
|
||||
return notificationId;
|
||||
}
|
||||
|
||||
@@ -421,8 +525,8 @@ class DatabaseHelper {
|
||||
Future<void> incrementRetryCount(int notificationId) async {
|
||||
Database db = await database;
|
||||
await db.rawUpdate('''
|
||||
UPDATE $notificationTrackingTable
|
||||
SET retryCount = retryCount + 1,
|
||||
UPDATE $notificationTrackingTable
|
||||
SET retryCount = retryCount + 1,
|
||||
lastRetryTime = ?,
|
||||
status = 'retrying'
|
||||
WHERE notificationId = ?
|
||||
@@ -469,4 +573,130 @@ class DatabaseHelper {
|
||||
whereArgs: [supplementId],
|
||||
);
|
||||
}
|
||||
|
||||
// Sync metadata operations
|
||||
Future<void> setSyncMetadata(String key, String value) async {
|
||||
Database db = await database;
|
||||
await db.rawInsert('''
|
||||
INSERT OR REPLACE INTO $syncMetadataTable (key, value, lastUpdated)
|
||||
VALUES (?, ?, ?)
|
||||
''', [key, value, DateTime.now().toIso8601String()]);
|
||||
}
|
||||
|
||||
Future<String?> getSyncMetadata(String key) async {
|
||||
Database db = await database;
|
||||
List<Map<String, dynamic>> result = await db.query(
|
||||
syncMetadataTable,
|
||||
where: 'key = ?',
|
||||
whereArgs: [key],
|
||||
);
|
||||
return result.isNotEmpty ? result.first['value'] : null;
|
||||
}
|
||||
|
||||
Future<void> deleteSyncMetadata(String key) async {
|
||||
Database db = await database;
|
||||
await db.delete(
|
||||
syncMetadataTable,
|
||||
where: 'key = ?',
|
||||
whereArgs: [key],
|
||||
);
|
||||
}
|
||||
|
||||
// Device info operations
|
||||
Future<void> setDeviceInfo(String deviceId, String deviceName) async {
|
||||
Database db = await database;
|
||||
await db.rawInsert('''
|
||||
INSERT OR REPLACE INTO $deviceInfoTable (deviceId, deviceName, lastSyncTime, createdAt)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', [deviceId, deviceName, null, DateTime.now().toIso8601String()]);
|
||||
}
|
||||
|
||||
Future<void> updateLastSyncTime(String deviceId) async {
|
||||
Database db = await database;
|
||||
await db.update(
|
||||
deviceInfoTable,
|
||||
{'lastSyncTime': DateTime.now().toIso8601String()},
|
||||
where: 'deviceId = ?',
|
||||
whereArgs: [deviceId],
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getDeviceInfo(String deviceId) async {
|
||||
Database db = await database;
|
||||
List<Map<String, dynamic>> result = await db.query(
|
||||
deviceInfoTable,
|
||||
where: 'deviceId = ?',
|
||||
whereArgs: [deviceId],
|
||||
);
|
||||
return result.isNotEmpty ? result.first : null;
|
||||
}
|
||||
|
||||
// Sync-specific queries
|
||||
Future<List<Supplement>> getModifiedSupplements() async {
|
||||
Database db = await database;
|
||||
List<Map<String, dynamic>> maps = await db.query(
|
||||
supplementsTable,
|
||||
where: 'syncStatus IN (?, ?)',
|
||||
whereArgs: [SyncStatus.pending.name, SyncStatus.modified.name],
|
||||
orderBy: 'lastModified ASC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => Supplement.fromMap(maps[i]));
|
||||
}
|
||||
|
||||
Future<List<SupplementIntake>> getModifiedIntakes() async {
|
||||
Database db = await database;
|
||||
List<Map<String, dynamic>> maps = await db.query(
|
||||
intakesTable,
|
||||
where: 'syncStatus IN (?, ?)',
|
||||
whereArgs: [SyncStatus.pending.name, SyncStatus.modified.name],
|
||||
orderBy: 'lastModified ASC',
|
||||
);
|
||||
return List.generate(maps.length, (i) => SupplementIntake.fromMap(maps[i]));
|
||||
}
|
||||
|
||||
Future<void> markSupplementAsSynced(String syncId) async {
|
||||
Database db = await database;
|
||||
await db.update(
|
||||
supplementsTable,
|
||||
{'syncStatus': SyncStatus.synced.name},
|
||||
where: 'syncId = ?',
|
||||
whereArgs: [syncId],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> markIntakeAsSynced(String syncId) async {
|
||||
Database db = await database;
|
||||
await db.update(
|
||||
intakesTable,
|
||||
{'syncStatus': SyncStatus.synced.name},
|
||||
where: 'syncId = ?',
|
||||
whereArgs: [syncId],
|
||||
);
|
||||
}
|
||||
|
||||
Future<Supplement?> getSupplementBySyncId(String syncId) async {
|
||||
Database db = await database;
|
||||
List<Map<String, dynamic>> maps = await db.query(
|
||||
supplementsTable,
|
||||
where: 'syncId = ?',
|
||||
whereArgs: [syncId],
|
||||
);
|
||||
if (maps.isNotEmpty) {
|
||||
return Supplement.fromMap(maps.first);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<SupplementIntake?> getIntakeBySyncId(String syncId) async {
|
||||
Database db = await database;
|
||||
List<Map<String, dynamic>> maps = await db.query(
|
||||
intakesTable,
|
||||
where: 'syncId = ?',
|
||||
whereArgs: [syncId],
|
||||
);
|
||||
if (maps.isNotEmpty) {
|
||||
return SupplementIntake.fromMap(maps.first);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user