Add offline meal journal and secure sync

This commit is contained in:
Hamza-Ayed
2026-10-04 02:13:23 +03:00
parent d892e3d981
commit 27fc2d5eb1
12 changed files with 936 additions and 23 deletions
+105 -1
View File
@@ -5,12 +5,13 @@ import 'package:path/path.dart';
import '../models/gps_coordinate.dart';
import '../models/workout.dart';
import '../models/workout_segment.dart';
import '../models/meal_entry.dart';
/// خدمة قاعدة البيانات المحلية
/// تدير كل جداول SQLite: الإحداثيات، التمارين، المقاطع، طابور المزامنة
class DatabaseService extends GetxService {
static const String _dbName = 'sportpath_tracker.db';
static const int _dbVersion = 3;
static const int _dbVersion = 4;
late Database _db;
@@ -142,6 +143,8 @@ class DatabaseService extends GetxService {
'CREATE INDEX idx_sync_workout ON sync_queue(workout_id)',
);
await _createMealTable(db);
debugPrint('[DB] All tables created successfully');
}
@@ -165,6 +168,33 @@ class DatabaseService extends GetxService {
'CREATE INDEX IF NOT EXISTS idx_sync_workout ON sync_queue(workout_id)',
);
}
if (oldVersion < 4) await _createMealTable(db);
}
Future<void> _createMealTable(DatabaseExecutor db) async {
await db.execute('''
CREATE TABLE IF NOT EXISTS meal_entries (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
meal_type TEXT NOT NULL CHECK(meal_type IN ('breakfast','lunch','dinner','snack')),
calories REAL NOT NULL CHECK(calories >= 0 AND calories <= 10000),
protein_grams REAL NOT NULL DEFAULT 0 CHECK(protein_grams >= 0 AND protein_grams <= 1000),
carbohydrate_grams REAL NOT NULL DEFAULT 0 CHECK(carbohydrate_grams >= 0 AND carbohydrate_grams <= 1000),
fat_grams REAL NOT NULL DEFAULT 0 CHECK(fat_grams >= 0 AND fat_grams <= 1000),
consumed_at TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'manual' CHECK(source IN ('manual','photo_ai')),
notes TEXT NOT NULL DEFAULT '',
sync_status TEXT NOT NULL DEFAULT 'pending' CHECK(sync_status IN ('pending','synced','failed')),
last_sync_error TEXT,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)
''');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_meals_consumed_at ON meal_entries(consumed_at DESC)',
);
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_meals_sync_status ON meal_entries(sync_status, consumed_at)',
);
}
// ─── GPS Coordinates CRUD ─────────────────────────────────────
@@ -576,6 +606,80 @@ class DatabaseService extends GetxService {
return result.isNotEmpty ? result.first : null;
}
Future<void> saveMeal(MealEntry meal) async {
await _db.insert(
'meal_entries',
{...meal.toMap(), 'updated_at': DateTime.now().toUtc().toIso8601String()},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<void> mergeSyncedMeal(MealEntry meal) async {
final local = await _db.query(
'meal_entries',
columns: ['sync_status'],
where: 'id = ?',
whereArgs: [meal.id],
limit: 1,
);
if (local.isNotEmpty &&
(local.first['sync_status'] == MealSyncStatus.pending.name ||
local.first['sync_status'] == MealSyncStatus.failed.name)) {
return;
}
meal.syncStatus = MealSyncStatus.synced;
await saveMeal(meal);
}
Future<List<MealEntry>> getMealsForDay(DateTime day) async {
final start = DateTime(day.year, day.month, day.day).toUtc();
final end = DateTime(day.year, day.month, day.day + 1).toUtc();
final rows = await _db.query(
'meal_entries',
where: 'consumed_at >= ? AND consumed_at < ?',
whereArgs: [start.toIso8601String(), end.toIso8601String()],
orderBy: 'consumed_at DESC',
);
return rows.map(MealEntry.fromMap).toList(growable: false);
}
Future<List<MealEntry>> getPendingMeals({int limit = 100}) async {
final rows = await _db.query(
'meal_entries',
where: "sync_status IN ('pending','failed')",
orderBy: 'consumed_at ASC',
limit: limit,
);
return rows.map(MealEntry.fromMap).toList(growable: false);
}
Future<int> getPendingMealCount() async {
final result = await _db.rawQuery(
"SELECT COUNT(*) AS total FROM meal_entries WHERE sync_status IN ('pending','failed')",
);
return Sqflite.firstIntValue(result) ?? 0;
}
Future<void> updateMealSyncStatus(
String id,
MealSyncStatus status, {
String? error,
}) async {
await _db.update(
'meal_entries',
{
'sync_status': status.name,
'last_sync_error': error,
'updated_at': DateTime.now().toUtc().toIso8601String(),
},
where: 'id = ?',
whereArgs: [id],
);
}
Future<int> getMealCountForDay(DateTime day) async =>
(await getMealsForDay(day)).length;
@override
void onClose() {
_db.close();