diff --git a/lib/screens/add_supplement_screen.dart b/lib/screens/add_supplement_screen.dart index 6847afe..b5a2f51 100644 --- a/lib/screens/add_supplement_screen.dart +++ b/lib/screens/add_supplement_screen.dart @@ -557,21 +557,24 @@ class _AddSupplementScreenState extends State { void _saveSupplement() async { if (_formKey.currentState!.validate()) { // Validate that we have at least one ingredient with name and amount - final validIngredients = _ingredientControllers.where((controller) => - controller.nameController.text.trim().isNotEmpty && - (double.tryParse(controller.amountController.text) ?? 0) > 0 - ).map((controller) => Ingredient( - name: controller.nameController.text.trim(), - amount: double.tryParse(controller.amountController.text) ?? 0.0, - unit: controller.selectedUnit, - syncId: const Uuid().v4(), - lastModified: DateTime.now(), - )).toList(); + final validIngredients = _ingredientControllers + .where((controller) => + controller.nameController.text.trim().isNotEmpty && + (double.tryParse(controller.amountController.text) ?? 0) > 0) + .map((controller) => Ingredient( + name: controller.nameController.text.trim(), + amount: double.tryParse(controller.amountController.text) ?? 0.0, + unit: controller.selectedUnit, + syncId: const Uuid().v4(), + lastModified: DateTime.now(), + )) + .toList(); if (validIngredients.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Please add at least one ingredient with name and amount'), + content: + Text('Please add at least one ingredient with name and amount'), ), ); return; @@ -580,14 +583,20 @@ class _AddSupplementScreenState extends State { final supplement = Supplement( id: widget.supplement?.id, name: _nameController.text.trim(), - brand: _brandController.text.trim().isNotEmpty ? _brandController.text.trim() : null, + brand: _brandController.text.trim().isNotEmpty + ? _brandController.text.trim() + : null, ingredients: validIngredients, numberOfUnits: int.parse(_numberOfUnitsController.text), unitType: _selectedUnitType, frequencyPerDay: _frequencyPerDay, reminderTimes: _reminderTimes, - notes: _notesController.text.trim().isNotEmpty ? _notesController.text.trim() : null, + notes: _notesController.text.trim().isNotEmpty + ? _notesController.text.trim() + : null, createdAt: widget.supplement?.createdAt ?? DateTime.now(), + syncId: widget.supplement?.syncId, // Preserve syncId on update + lastModified: DateTime.now(), // Always update lastModified on save ); final provider = context.read(); diff --git a/lib/services/database_sync_service.dart b/lib/services/database_sync_service.dart index 9ba1504..bc4b572 100644 --- a/lib/services/database_sync_service.dart +++ b/lib/services/database_sync_service.dart @@ -27,40 +27,40 @@ enum RecordSyncStatus { class DatabaseSyncService { static const String _remoteDbFileName = 'supplements.db'; - + // SharedPreferences keys for persistence static const String _keyServerUrl = 'sync_server_url'; static const String _keyUsername = 'sync_username'; static const String _keyPassword = 'sync_password'; static const String _keyRemotePath = 'sync_remote_path'; - + Client? _client; String? _remotePath; - + // Store configuration values String? _serverUrl; - String? _username; + String? _username; String? _password; String? _configuredRemotePath; - + final DatabaseHelper _databaseHelper = DatabaseHelper.instance; - + SyncStatus _status = SyncStatus.idle; String? _lastError; DateTime? _lastSyncTime; - + // Getters SyncStatus get status => _status; String? get lastError => _lastError; DateTime? get lastSyncTime => _lastSyncTime; bool get isConfigured => _client != null; - + // Configuration getters String? get serverUrl => _serverUrl; String? get username => _username; String? get password => _password; String? get remotePath => _configuredRemotePath; - + // Callbacks for UI updates Function(SyncStatus)? onStatusChanged; Function(String)? onError; @@ -78,11 +78,11 @@ class DatabaseSyncService { _username = prefs.getString(_keyUsername); _password = prefs.getString(_keyPassword); _configuredRemotePath = prefs.getString(_keyRemotePath); - + // If we have saved configuration, set up the client if (_serverUrl != null && _username != null && _password != null && _configuredRemotePath != null) { _remotePath = _configuredRemotePath!.endsWith('/') ? _configuredRemotePath : '$_configuredRemotePath/'; - + _client = newClient( _serverUrl!, user: _username!, @@ -123,9 +123,9 @@ class DatabaseSyncService { _username = username; _password = password; _configuredRemotePath = remotePath; - + _remotePath = remotePath.endsWith('/') ? remotePath : '$remotePath/'; - + _client = newClient( serverUrl, user: username, @@ -139,7 +139,7 @@ class DatabaseSyncService { Future testConnection() async { if (_client == null) return false; - + try { await _client!.ping(); return true; @@ -157,26 +157,26 @@ class DatabaseSyncService { } _setStatus(SyncStatus.downloading); - + try { // Step 1: Download remote database (if it exists) final remoteDbPath = await _downloadRemoteDatabase(); - + // Step 2: Merge databases _setStatus(SyncStatus.merging); await _mergeDatabases(remoteDbPath); - + // Step 3: Upload merged database _setStatus(SyncStatus.uploading); await _uploadLocalDatabase(); - + // Step 4: Cleanup - for now we'll skip cleanup to avoid file issues // TODO: Implement proper cleanup once file operations are working - + _lastSyncTime = DateTime.now(); _setStatus(SyncStatus.completed); onSyncCompleted?.call(); - + } catch (e) { _lastError = e.toString(); _setStatus(SyncStatus.error); @@ -193,35 +193,35 @@ class DatabaseSyncService { // Check if remote database exists final files = await _client!.readDir(_remotePath!); final remoteDbExists = files.any((file) => file.name == _remoteDbFileName); - + if (!remoteDbExists) { if (kDebugMode) { print('SupplementsLog: No remote database found, will upload local database'); } return null; } - + if (kDebugMode) { print('SupplementsLog: Remote database found, downloading...'); } - + // Download the remote database final remoteDbBytes = await _client!.read('$_remotePath$_remoteDbFileName'); - + // Create a temporary file path for the downloaded database final tempDir = await getDatabasesPath(); final tempDbPath = join(tempDir, 'remote_supplements.db'); - + // Write the downloaded database to a temporary file final tempFile = io.File(tempDbPath); await tempFile.writeAsBytes(remoteDbBytes); - + if (kDebugMode) { print('SupplementsLog: Downloaded remote database (${remoteDbBytes.length} bytes) to: $tempDbPath'); } - + return tempDbPath; - + } catch (e) { if (kDebugMode) { print('SupplementsLog: Failed to download remote database: $e'); @@ -237,20 +237,20 @@ class DatabaseSyncService { } return; } - + if (kDebugMode) { print('SupplementsLog: Starting database merge from: $remoteDbPath'); } - + final localDb = await _databaseHelper.database; final remoteDb = await openDatabase(remoteDbPath, readOnly: true); - + try { // Check what tables exist in remote database if (kDebugMode) { final tables = await remoteDb.rawQuery("SELECT name FROM sqlite_master WHERE type='table'"); print('SupplementsLog: Remote database tables: ${tables.map((t) => t['name']).toList()}'); - + // Count records in each table try { final supplementCount = await remoteDb.rawQuery('SELECT COUNT(*) as count FROM supplements'); @@ -258,7 +258,7 @@ class DatabaseSyncService { } catch (e) { print('SupplementsLog: Error counting supplements: $e'); } - + try { final intakeCount = await remoteDb.rawQuery('SELECT COUNT(*) as count FROM supplement_intakes'); print('SupplementsLog: Remote intakes count: ${intakeCount.first['count']}'); @@ -266,17 +266,17 @@ class DatabaseSyncService { print('SupplementsLog: Error counting intakes: $e'); } } - + // Merge supplements await _mergeSupplements(localDb, remoteDb); - + // Merge intakes await _mergeIntakes(localDb, remoteDb); - + if (kDebugMode) { print('SupplementsLog: Database merge completed successfully'); } - + } finally { await remoteDb.close(); } @@ -286,52 +286,62 @@ class DatabaseSyncService { if (kDebugMode) { print('SupplementsLog: Starting supplement merge...'); } - + // Get all supplements from remote database final remoteMaps = await remoteDb.query('supplements'); - final remoteSupplements = remoteMaps.map((map) => Supplement.fromMap(map)).toList(); - + final remoteSupplements = + remoteMaps.map((map) => Supplement.fromMap(map)).toList(); + if (kDebugMode) { - print('SupplementsLog: Found ${remoteSupplements.length} supplements in remote database'); + print( + 'SupplementsLog: Found ${remoteSupplements.length} supplements in remote database'); for (final supplement in remoteSupplements) { - print('SupplementsLog: Remote supplement: ${supplement.name} (syncId: ${supplement.syncId}, deleted: ${supplement.isDeleted})'); + print( + 'SupplementsLog: Remote supplement: ${supplement.name} (syncId: ${supplement.syncId}, deleted: ${supplement.isDeleted})'); } } - + for (final remoteSupplement in remoteSupplements) { if (remoteSupplement.syncId.isEmpty) { if (kDebugMode) { - print('SupplementsLog: Skipping supplement ${remoteSupplement.name} - no syncId'); + print( + 'SupplementsLog: Skipping supplement ${remoteSupplement.name} - no syncId'); } continue; } - + // Find existing supplement by syncId final existingMaps = await localDb.query( 'supplements', where: 'syncId = ?', whereArgs: [remoteSupplement.syncId], ); - + if (existingMaps.isEmpty) { // New supplement from remote - insert it if (!remoteSupplement.isDeleted) { - final supplementToInsert = remoteSupplement.copyWith(id: null); - await localDb.insert('supplements', supplementToInsert.toMap()); + // Manually create a new map without the id to ensure it's null + final mapToInsert = remoteSupplement.toMap(); + mapToInsert.remove('id'); + await localDb.insert('supplements', mapToInsert); if (kDebugMode) { - print('SupplementsLog: ✓ Inserted new supplement: ${remoteSupplement.name}'); + print( + 'SupplementsLog: ✓ Inserted new supplement: ${remoteSupplement.name}'); } } else { if (kDebugMode) { - print('SupplementsLog: Skipping deleted supplement: ${remoteSupplement.name}'); + print( + 'SupplementsLog: Skipping deleted supplement: ${remoteSupplement.name}'); } } } else { // Existing supplement - update if remote is newer final existingSupplement = Supplement.fromMap(existingMaps.first); - - if (remoteSupplement.lastModified.isAfter(existingSupplement.lastModified)) { - final supplementToUpdate = remoteSupplement.copyWith(id: existingSupplement.id); + + if (remoteSupplement.lastModified + .isAfter(existingSupplement.lastModified)) { + final supplementToUpdate = + remoteSupplement.copyWith(id: existingSupplement.id); await localDb.update( 'supplements', supplementToUpdate.toMap(), @@ -339,16 +349,18 @@ class DatabaseSyncService { whereArgs: [existingSupplement.id], ); if (kDebugMode) { - print('SupplementsLog: ✓ Updated supplement: ${remoteSupplement.name}'); + print( + 'SupplementsLog: ✓ Updated supplement: ${remoteSupplement.name}'); } } else { if (kDebugMode) { - print('SupplementsLog: Local supplement ${remoteSupplement.name} is newer, keeping local version'); + print( + 'SupplementsLog: Local supplement ${remoteSupplement.name} is newer, keeping local version'); } } } } - + if (kDebugMode) { print('SupplementsLog: Supplement merge completed'); } @@ -358,15 +370,15 @@ class DatabaseSyncService { if (kDebugMode) { print('SupplementsLog: Starting intake merge...'); } - + // Get all intakes from remote database final remoteMaps = await remoteDb.query('supplement_intakes'); final remoteIntakes = remoteMaps.map((map) => SupplementIntake.fromMap(map)).toList(); - + if (kDebugMode) { print('SupplementsLog: Found ${remoteIntakes.length} intakes in remote database'); } - + for (final remoteIntake in remoteIntakes) { if (remoteIntake.syncId.isEmpty) { if (kDebugMode) { @@ -374,14 +386,14 @@ class DatabaseSyncService { } continue; } - + // Find existing intake by syncId final existingMaps = await localDb.query( 'supplement_intakes', where: 'syncId = ?', whereArgs: [remoteIntake.syncId], ); - + if (existingMaps.isEmpty) { // New intake from remote - need to find local supplement ID if (!remoteIntake.isDeleted) { @@ -408,7 +420,7 @@ class DatabaseSyncService { } else { // Existing intake - update if remote is newer final existingIntake = SupplementIntake.fromMap(existingMaps.first); - + if (remoteIntake.lastModified.isAfter(existingIntake.lastModified)) { final intakeToUpdate = remoteIntake.copyWith(id: existingIntake.id); await localDb.update( @@ -427,7 +439,7 @@ class DatabaseSyncService { } } } - + if (kDebugMode) { print('SupplementsLog: Intake merge completed'); } @@ -440,20 +452,20 @@ class DatabaseSyncService { where: 'id = ?', whereArgs: [remoteSupplementId], ); - + if (remoteSupplementMaps.isEmpty) return null; - + final remoteSupplement = Supplement.fromMap(remoteSupplementMaps.first); - + // Find the local supplement with the same syncId final localSupplementMaps = await localDb.query( 'supplements', where: 'syncId = ?', whereArgs: [remoteSupplement.syncId], ); - + if (localSupplementMaps.isEmpty) return null; - + return localSupplementMaps.first['id'] as int; } @@ -462,27 +474,27 @@ class DatabaseSyncService { // Get the local database path final localDb = await _databaseHelper.database; final dbPath = localDb.path; - + if (kDebugMode) { print('SupplementsLog: Reading database from: $dbPath'); } - + // Read the database file final dbFile = io.File(dbPath); if (!await dbFile.exists()) { throw Exception('Database file not found at: $dbPath'); } - + final dbBytes = await dbFile.readAsBytes(); - + if (kDebugMode) { print('SupplementsLog: Database file size: ${dbBytes.length} bytes'); } - + if (dbBytes.isEmpty) { throw Exception('Database file is empty'); } - + // Ensure remote directory exists try { await _client!.readDir(_remotePath!); @@ -492,15 +504,15 @@ class DatabaseSyncService { } await _client!.mkdir(_remotePath!); } - + // Upload the database file final remoteUrl = '$_remotePath$_remoteDbFileName'; await _client!.write(remoteUrl, dbBytes); - + if (kDebugMode) { print('SupplementsLog: Successfully uploaded database (${dbBytes.length} bytes) to: $remoteUrl'); } - + } catch (e) { if (kDebugMode) { print('SupplementsLog: Failed to upload database: $e');