Prepare secure config and reliable workout sync
This commit is contained in:
@@ -10,16 +10,15 @@ import '../models/workout_segment.dart';
|
||||
/// تدير كل جداول SQLite: الإحداثيات، التمارين، المقاطع، طابور المزامنة
|
||||
class DatabaseService extends GetxService {
|
||||
static const String _dbName = 'sportpath_tracker.db';
|
||||
static const int _dbVersion = 2;
|
||||
static const int _dbVersion = 3;
|
||||
|
||||
late Database _db;
|
||||
|
||||
Database get db => _db;
|
||||
|
||||
@override
|
||||
Future<void> onInit() async {
|
||||
super.onInit();
|
||||
Future<DatabaseService> init() async {
|
||||
await _initDatabase();
|
||||
return this;
|
||||
}
|
||||
|
||||
Future<void> _initDatabase() async {
|
||||
@@ -28,6 +27,9 @@ class DatabaseService extends GetxService {
|
||||
_db = await openDatabase(
|
||||
dbPath,
|
||||
version: _dbVersion,
|
||||
onConfigure: (db) async {
|
||||
await db.execute('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
onCreate: _onCreate,
|
||||
onUpgrade: _onUpgrade,
|
||||
);
|
||||
@@ -136,6 +138,9 @@ class DatabaseService extends GetxService {
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_sync_status ON sync_queue(status)',
|
||||
);
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_sync_workout ON sync_queue(workout_id)',
|
||||
);
|
||||
|
||||
debugPrint('[DB] All tables created successfully');
|
||||
}
|
||||
@@ -155,6 +160,11 @@ class DatabaseService extends GetxService {
|
||||
// Columns may already exist
|
||||
}
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
await db.execute(
|
||||
'CREATE INDEX IF NOT EXISTS idx_sync_workout ON sync_queue(workout_id)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── GPS Coordinates CRUD ─────────────────────────────────────
|
||||
@@ -234,6 +244,73 @@ class DatabaseService extends GetxService {
|
||||
);
|
||||
}
|
||||
|
||||
/// Atomically persist a finished workout, its segments, and its sync outbox item.
|
||||
Future<void> saveCompletedWorkout(Workout workout, String payload) async {
|
||||
await _db.transaction((txn) async {
|
||||
final saved = await txn.query(
|
||||
'workouts',
|
||||
columns: ['id'],
|
||||
where: 'id = ?',
|
||||
whereArgs: [workout.id],
|
||||
limit: 1,
|
||||
);
|
||||
|
||||
if (saved.isEmpty) {
|
||||
await txn.insert('workouts', workout.toMap());
|
||||
} else {
|
||||
final existingWorkout = await txn.query(
|
||||
'workouts',
|
||||
columns: ['sync_status', 'synced_at'],
|
||||
where: 'id = ?',
|
||||
whereArgs: [workout.id],
|
||||
limit: 1,
|
||||
);
|
||||
final values = workout.toMap()
|
||||
..['sync_status'] = existingWorkout.first['sync_status']
|
||||
..['synced_at'] = existingWorkout.first['synced_at'];
|
||||
await txn.update(
|
||||
'workouts',
|
||||
values,
|
||||
where: 'id = ?',
|
||||
whereArgs: [workout.id],
|
||||
);
|
||||
}
|
||||
|
||||
await txn.delete(
|
||||
'workout_segments',
|
||||
where: 'workout_id = ?',
|
||||
whereArgs: [workout.id],
|
||||
);
|
||||
for (final segment in workout.segments) {
|
||||
final values = segment.toMap()..['workout_id'] = workout.id;
|
||||
await txn.insert('workout_segments', values);
|
||||
}
|
||||
|
||||
final queued = await txn.query(
|
||||
'sync_queue',
|
||||
columns: ['id', 'status'],
|
||||
where: 'workout_id = ?',
|
||||
whereArgs: [workout.id],
|
||||
orderBy: 'id ASC',
|
||||
limit: 1,
|
||||
);
|
||||
if (queued.isEmpty) {
|
||||
await txn.insert('sync_queue', {
|
||||
'workout_id': workout.id,
|
||||
'payload': payload,
|
||||
'status': 'queued',
|
||||
});
|
||||
} else if (queued.first['status'] != 'sent') {
|
||||
await txn.update(
|
||||
'sync_queue',
|
||||
{'payload': payload},
|
||||
where: 'id = ?',
|
||||
whereArgs: [queued.first['id']],
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Update workout fields
|
||||
Future<void> updateWorkout(Workout workout) async {
|
||||
await _db.update(
|
||||
@@ -369,33 +446,49 @@ class DatabaseService extends GetxService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> getPendingSyncCount() async {
|
||||
final result = await _db.rawQuery(
|
||||
"SELECT COUNT(*) AS cnt FROM sync_queue WHERE status IN ('queued', 'failed')",
|
||||
);
|
||||
return Sqflite.firstIntValue(result) ?? 0;
|
||||
}
|
||||
|
||||
Future<void> retryFailedSync(String workoutId) async {
|
||||
await _db.update(
|
||||
'sync_queue',
|
||||
{
|
||||
'status': 'queued',
|
||||
'retry_count': 0,
|
||||
'last_attempt': null,
|
||||
'error_message': null,
|
||||
},
|
||||
where: "workout_id = ? AND status = 'failed'",
|
||||
whereArgs: [workoutId],
|
||||
);
|
||||
}
|
||||
|
||||
/// Update sync queue item status
|
||||
Future<void> updateSyncQueueItem(
|
||||
int id,
|
||||
String status, {
|
||||
String? errorMessage,
|
||||
}) async {
|
||||
int newRetryCount = 0;
|
||||
|
||||
// سحبنا المنطق خارج الـ Map ليكون واضحاً ومقروءاً
|
||||
if (status == 'failed') {
|
||||
final currentItem =
|
||||
await _db.query('sync_queue', where: 'id = ?', whereArgs: [id]);
|
||||
if (currentItem.isNotEmpty) {
|
||||
newRetryCount = (currentItem.first['retry_count'] as int? ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
await _db.update(
|
||||
'sync_queue',
|
||||
{
|
||||
'status': status,
|
||||
'last_attempt': DateTime.now().toIso8601String(),
|
||||
'retry_count': newRetryCount, // وضعنا القيمة الجاهزة هنا
|
||||
if (errorMessage != null) 'error_message': errorMessage,
|
||||
},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
await _db.rawUpdate(
|
||||
'''
|
||||
UPDATE sync_queue
|
||||
SET status = ?,
|
||||
last_attempt = ?,
|
||||
retry_count = retry_count + CASE WHEN ? = 'failed' THEN 1 ELSE 0 END,
|
||||
error_message = ?
|
||||
WHERE id = ?
|
||||
''',
|
||||
[
|
||||
status,
|
||||
DateTime.now().toUtc().toIso8601String(),
|
||||
status,
|
||||
errorMessage,
|
||||
id,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user