// lib/services/api_service.dart import 'dart:io'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as p; import 'package:sqflite/sqflite.dart'; import 'package:path_provider/path_provider.dart'; import 'package:intl/intl.dart'; import 'package:environment_monitoring_app/services/base_api_service.dart'; import 'package:environment_monitoring_app/services/telegram_service.dart'; import 'package:environment_monitoring_app/models/in_situ_sampling_data.dart'; import 'package:environment_monitoring_app/models/tarball_data.dart'; // ======================================================================= // Part 1: Unified API Service // ======================================================================= /// A unified service that consolidates all API interactions for the application. /// It is organized by feature (e.g., marine, river) for clarity and provides /// a central point for data synchronization. class ApiService { final BaseApiService _baseService = BaseApiService(); final DatabaseHelper _dbHelper = DatabaseHelper(); late final MarineApiService marine; late final RiverApiService river; late final AirApiService air; static const String imageBaseUrl = 'https://dev14.pstw.com.my/'; ApiService() { marine = MarineApiService(_baseService); river = RiverApiService(_baseService); air = AirApiService(_baseService); } // --- Core API Methods (Unchanged) --- Future> login(String email, String password) { return _baseService.post('auth/login', {'email': email, 'password': password}); } Future> register({ required String username, String? firstName, String? lastName, required String email, required String password, String? phoneNumber, int? departmentId, int? companyId, int? positionId, }) { final Map body = { 'username': username, 'email': email, 'password': password, 'first_name': firstName ?? '', 'last_name': lastName ?? '', 'phone_number': phoneNumber ?? '', 'department_id': departmentId, 'company_id': companyId, 'position_id': positionId, }; body.removeWhere((key, value) => value == null); return _baseService.post('auth/register', body); } Future> post(String endpoint, Map data) { return _baseService.post(endpoint, data); } Future> getProfile() => _baseService.get('profile'); Future> getAllUsers() => _baseService.get('users'); Future> getAllDepartments() => _baseService.get('departments'); Future> getAllCompanies() => _baseService.get('companies'); Future> getAllPositions() => _baseService.get('positions'); Future> getAllStates() => _baseService.get('states'); Future> sendTelegramAlert({ required String chatId, required String message, }) { return _baseService.post('marine/telegram-alert', { 'chat_id': chatId, 'message': message, }); } Future downloadProfilePicture(String imageUrl, String localPath) async { try { final response = await http.get(Uri.parse(imageUrl)); if (response.statusCode == 200) { final File file = File(localPath); await file.parent.create(recursive: true); await file.writeAsBytes(response.bodyBytes); return file; } } catch (e) { debugPrint('Error downloading profile picture: $e'); } return null; } Future> uploadProfilePicture(File imageFile) { return _baseService.postMultipart( endpoint: 'profile/upload-picture', fields: {}, files: {'profile_picture': imageFile} ); } Future> refreshProfile() async { debugPrint('ApiService: Refreshing profile data from server...'); final result = await getProfile(); if (result['success'] == true && result['data'] != null) { await _dbHelper.saveProfile(result['data']); debugPrint('ApiService: Profile data refreshed and saved to local DB.'); } return result; } // --- REWRITTEN FOR DELTA SYNC --- /// Helper method to make a delta-sync API call. Future> _fetchDelta(String endpoint, String? lastSyncTimestamp) { String url = endpoint; if (lastSyncTimestamp != null) { // Append the 'since' parameter to the URL for delta requests url += '?since=$lastSyncTimestamp'; } return _baseService.get(url); } /// Orchestrates a full DELTA sync from the server to the local database. Future> syncAllData({String? lastSyncTimestamp}) async { debugPrint('ApiService: Starting DELTA data sync. Since: $lastSyncTimestamp'); try { // Defines all data types to sync, their endpoints, and their DB handlers. final syncTasks = { 'profile': {'endpoint': 'profile', 'handler': (d, id) async { if (d.isNotEmpty) await _dbHelper.saveProfile(d.first); }}, 'allUsers': {'endpoint': 'users', 'handler': (d, id) async { await _dbHelper.upsertUsers(d); await _dbHelper.deleteUsers(id); }}, 'tarballStations': {'endpoint': 'marine/tarball/stations', 'handler': (d, id) async { await _dbHelper.upsertTarballStations(d); await _dbHelper.deleteTarballStations(id); }}, 'manualStations': {'endpoint': 'marine/manual/stations', 'handler': (d, id) async { await _dbHelper.upsertManualStations(d); await _dbHelper.deleteManualStations(id); }}, 'tarballClassifications': {'endpoint': 'marine/tarball/classifications', 'handler': (d, id) async { await _dbHelper.upsertTarballClassifications(d); await _dbHelper.deleteTarballClassifications(id); }}, 'riverManualStations': {'endpoint': 'river/manual-stations', 'handler': (d, id) async { await _dbHelper.upsertRiverManualStations(d); await _dbHelper.deleteRiverManualStations(id); }}, 'riverTriennialStations': {'endpoint': 'river/triennial-stations', 'handler': (d, id) async { await _dbHelper.upsertRiverTriennialStations(d); await _dbHelper.deleteRiverTriennialStations(id); }}, 'departments': {'endpoint': 'departments', 'handler': (d, id) async { await _dbHelper.upsertDepartments(d); await _dbHelper.deleteDepartments(id); }}, 'companies': {'endpoint': 'companies', 'handler': (d, id) async { await _dbHelper.upsertCompanies(d); await _dbHelper.deleteCompanies(id); }}, 'positions': {'endpoint': 'positions', 'handler': (d, id) async { await _dbHelper.upsertPositions(d); await _dbHelper.deletePositions(id); }}, 'airManualStations': {'endpoint': 'air/manual-stations', 'handler': (d, id) async { await _dbHelper.upsertAirManualStations(d); await _dbHelper.deleteAirManualStations(id); }}, 'airClients': {'endpoint': 'air/clients', 'handler': (d, id) async { await _dbHelper.upsertAirClients(d); await _dbHelper.deleteAirClients(id); }}, 'states': {'endpoint': 'states', 'handler': (d, id) async { await _dbHelper.upsertStates(d); await _dbHelper.deleteStates(id); }}, 'appSettings': {'endpoint': 'settings', 'handler': (d, id) async { await _dbHelper.upsertAppSettings(d); await _dbHelper.deleteAppSettings(id); }}, 'parameterLimits': {'endpoint': 'parameter-limits', 'handler': (d, id) async { await _dbHelper.upsertParameterLimits(d); await _dbHelper.deleteParameterLimits(id); }}, // --- ADDED: New sync tasks for independent API and FTP configurations --- 'apiConfigs': {'endpoint': 'api-configs', 'handler': (d, id) async { await _dbHelper.upsertApiConfigs(d); await _dbHelper.deleteApiConfigs(id); }}, 'ftpConfigs': {'endpoint': 'ftp-configs', 'handler': (d, id) async { await _dbHelper.upsertFtpConfigs(d); await _dbHelper.deleteFtpConfigs(id); }}, }; // Fetch all deltas in parallel final fetchFutures = syncTasks.map((key, value) => MapEntry(key, _fetchDelta(value['endpoint'] as String, lastSyncTimestamp))); final results = await Future.wait(fetchFutures.values); final resultData = Map.fromIterables(fetchFutures.keys, results); // Process and save all changes for (var entry in resultData.entries) { final key = entry.key; final result = entry.value; if (result['success'] == true && result['data'] != null) { // The profile endpoint has a different structure, handle it separately. if (key == 'profile') { await (syncTasks[key]!['handler'] as Function)([result['data']], []); } else { final updated = List>.from(result['data']['updated'] ?? []); final deleted = List.from(result['data']['deleted'] ?? []); await (syncTasks[key]!['handler'] as Function)(updated, deleted); } } else { debugPrint('ApiService: Failed to sync $key. Message: ${result['message']}'); } } debugPrint('ApiService: Delta sync complete.'); return {'success': true, 'message': 'Delta sync successful.'}; } catch (e) { debugPrint('ApiService: Delta data sync failed: $e'); return {'success': false, 'message': 'Data sync failed: $e'}; } } } // ======================================================================= // Part 2: Feature-Specific API Services (Refactored to include Telegram) // ======================================================================= class AirApiService { // ... (No changes needed here) final BaseApiService _baseService; AirApiService(this._baseService); Future> getManualStations() => _baseService.get('air/manual-stations'); Future> getClients() => _baseService.get('air/clients'); Future> uploadInstallationImages({ required String airManId, required Map files, }) { return _baseService.postMultipart( endpoint: 'air/manual/installation-images', fields: {'air_man_id': airManId}, files: files, ); } Future> uploadCollectionImages({ required String airManId, required Map files, }) { return _baseService.postMultipart( endpoint: 'air/manual/collection-images', fields: {'air_man_id': airManId}, files: files, ); } } class MarineApiService { // --- ADDED: TelegramService instance --- final BaseApiService _baseService; final TelegramService _telegramService = TelegramService(); MarineApiService(this._baseService); Future> getTarballStations() => _baseService.get('marine/tarball/stations'); Future> getManualStations() => _baseService.get('marine/manual/stations'); Future> getTarballClassifications() => _baseService.get('marine/tarball/classifications'); // --- REVISED: Now includes appSettings parameter and triggers Telegram alert --- Future> submitTarballSample({ required Map formData, required Map imageFiles, required List>? appSettings, }) async { final dataResult = await _baseService.post('marine/tarball/sample', formData); if (dataResult['success'] != true) return {'status': 'L1', 'success': false, 'message': 'Failed to submit data: ${dataResult['message']}'}; final recordId = dataResult['data']?['autoid']; if (recordId == null) return {'status': 'L2', 'success': false, 'message': 'Data submitted, but failed to get a record ID.'}; final filesToUpload = {}; imageFiles.forEach((key, value) { if (value != null) filesToUpload[key] = value; }); if (filesToUpload.isEmpty) { _handleTarballSuccessAlert(formData, appSettings, isDataOnly: true); return {'status': 'L3', 'success': true, 'message': 'Data submitted successfully.', 'reportId': recordId}; } final imageResult = await _baseService.postMultipart(endpoint: 'marine/tarball/images', fields: {'autoid': recordId.toString()}, files: filesToUpload); if (imageResult['success'] != true) { // Still send the alert for data submission even if images fail _handleTarballSuccessAlert(formData, appSettings, isDataOnly: true); return {'status': 'L2', 'success': false, 'message': 'Data submitted, but image upload failed: ${imageResult['message']}', 'reportId': recordId}; } // On complete success, send the full alert _handleTarballSuccessAlert(formData, appSettings, isDataOnly: false); return {'status': 'L3', 'success': true, 'message': 'Data and images submitted successfully.', 'reportId': recordId}; } // --- ADDED: Helper method for Telegram alerts --- Future _handleTarballSuccessAlert(Map formData, List>? appSettings, {required bool isDataOnly}) async { debugPrint("Triggering Telegram alert logic..."); try { final message = _generateTarballAlertMessage(formData, isDataOnly: isDataOnly); final bool wasSent = await _telegramService.sendAlertImmediately('marine_tarball', message, appSettings); if (!wasSent) { await _telegramService.queueMessage('marine_tarball', message, appSettings); } } catch (e) { debugPrint("Failed to handle Tarball Telegram alert: $e"); } } // --- ADDED: Helper method to generate the Telegram message --- String _generateTarballAlertMessage(Map formData, {required bool isDataOnly}) { final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)"; final stationName = formData['tbl_station_name'] ?? 'N/A'; final stationCode = formData['tbl_station_code'] ?? 'N/A'; final classification = formData['classification_name'] ?? formData['classification_id'] ?? 'N/A'; final buffer = StringBuffer() ..writeln('✅ *Tarball Sample $submissionType Submitted:*') ..writeln() ..writeln('*Station Name & Code:* $stationName ($stationCode)') ..writeln('*Date of Submission:* ${formData['sampling_date']}') ..writeln('*Submitted by User:* ${formData['first_sampler_name'] ?? 'N/A'}') ..writeln('*Classification:* $classification') ..writeln('*Status of Submission:* Successful'); if (formData['distance_difference'] != null && double.tryParse(formData['distance_difference']!) != null && double.parse(formData['distance_difference']!) > 0) { buffer ..writeln() ..writeln('🔔 *Alert:*') ..writeln('*Distance from station:* ${(double.parse(formData['distance_difference']!) * 1000).toStringAsFixed(0)} meters'); if (formData['distance_difference_remarks'] != null && formData['distance_difference_remarks']!.isNotEmpty) { buffer.writeln('*Remarks for distance:* ${formData['distance_difference_remarks']}'); } } return buffer.toString(); } } class RiverApiService { // ... (No changes needed here) final BaseApiService _baseService; RiverApiService(this._baseService); Future> getManualStations() => _baseService.get('river/manual-stations'); Future> getTriennialStations() => _baseService.get('river/triennial-stations'); } // ======================================================================= // Part 3: Local Database Helper (Refactored for Delta Sync) // ======================================================================= class DatabaseHelper { static Database? _database; static const String _dbName = 'app_data.db'; // Incremented DB version to trigger the onUpgrade method static const int _dbVersion = 17; static const String _profileTable = 'user_profile'; static const String _usersTable = 'all_users'; static const String _tarballStationsTable = 'marine_tarball_stations'; static const String _manualStationsTable = 'marine_manual_stations'; static const String _riverManualStationsTable = 'river_manual_stations'; static const String _riverTriennialStationsTable = 'river_triennial_stations'; static const String _tarballClassificationsTable = 'marine_tarball_classifications'; static const String _departmentsTable = 'departments'; static const String _companiesTable = 'companies'; static const String _positionsTable = 'positions'; static const String _alertQueueTable = 'alert_queue'; static const String _airManualStationsTable = 'air_manual_stations'; static const String _airClientsTable = 'air_clients'; static const String _statesTable = 'states'; // Added new table constants static const String _appSettingsTable = 'app_settings'; static const String _parameterLimitsTable = 'manual_parameter_limits'; // --- ADDED: New tables for independent API and FTP configurations --- static const String _apiConfigsTable = 'api_configurations'; static const String _ftpConfigsTable = 'ftp_configurations'; // --- ADDED: New table for the manual retry queue --- static const String _retryQueueTable = 'retry_queue'; Future get database async { if (_database != null) return _database!; _database = await _initDB(); return _database!; } Future _initDB() async { String dbPath = p.join(await getDatabasesPath(), _dbName); return await openDatabase(dbPath, version: _dbVersion, onCreate: _onCreate, onUpgrade: _onUpgrade); } Future _onCreate(Database db, int version) async { await db.execute('CREATE TABLE $_profileTable(user_id INTEGER PRIMARY KEY, profile_json TEXT)'); await db.execute('CREATE TABLE $_usersTable(user_id INTEGER PRIMARY KEY, user_json TEXT)'); await db.execute('CREATE TABLE $_tarballStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)'); await db.execute('CREATE TABLE $_manualStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)'); await db.execute('CREATE TABLE $_riverManualStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)'); await db.execute('CREATE TABLE $_riverTriennialStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)'); await db.execute('CREATE TABLE $_tarballClassificationsTable(classification_id INTEGER PRIMARY KEY, classification_json TEXT)'); await db.execute('CREATE TABLE $_departmentsTable(department_id INTEGER PRIMARY KEY, department_json TEXT)'); await db.execute('CREATE TABLE $_companiesTable(company_id INTEGER PRIMARY KEY, company_json TEXT)'); await db.execute('CREATE TABLE $_positionsTable(position_id INTEGER PRIMARY KEY, position_json TEXT)'); await db.execute('''CREATE TABLE $_alertQueueTable (id INTEGER PRIMARY KEY AUTOINCREMENT, chat_id TEXT NOT NULL, message TEXT NOT NULL, created_at TEXT NOT NULL)'''); await db.execute('CREATE TABLE $_airManualStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)'); await db.execute('CREATE TABLE $_airClientsTable(client_id INTEGER PRIMARY KEY, client_json TEXT)'); await db.execute('CREATE TABLE $_statesTable(state_id INTEGER PRIMARY KEY, state_json TEXT)'); // Added create statements for new tables await db.execute('CREATE TABLE $_appSettingsTable(setting_id INTEGER PRIMARY KEY, setting_json TEXT)'); await db.execute('CREATE TABLE $_parameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)'); // --- ADDED: Create statements for new configuration tables --- await db.execute('CREATE TABLE $_apiConfigsTable(api_config_id INTEGER PRIMARY KEY, config_json TEXT)'); await db.execute('CREATE TABLE $_ftpConfigsTable(ftp_config_id INTEGER PRIMARY KEY, config_json TEXT)'); // --- ADDED: Create statement for the new retry queue table --- await db.execute(''' CREATE TABLE $_retryQueueTable( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, endpoint_or_path TEXT NOT NULL, payload TEXT, timestamp TEXT NOT NULL, status TEXT NOT NULL ) '''); } Future _onUpgrade(Database db, int oldVersion, int newVersion) async { if (oldVersion < 11) { await db.execute('CREATE TABLE IF NOT EXISTS $_airManualStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)'); await db.execute('CREATE TABLE IF NOT EXISTS $_airClientsTable(client_id INTEGER PRIMARY KEY, client_json TEXT)'); } if (oldVersion < 12) { await db.execute('CREATE TABLE IF NOT EXISTS $_statesTable(state_id INTEGER PRIMARY KEY, state_json TEXT)'); } if (oldVersion < 13) { await db.execute('CREATE TABLE IF NOT EXISTS $_appSettingsTable(setting_id INTEGER PRIMARY KEY, setting_json TEXT)'); await db.execute('CREATE TABLE IF NOT EXISTS $_parameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)'); } // --- ADDED: Upgrade logic for new configuration tables --- if (oldVersion < 16) { await db.execute('CREATE TABLE IF NOT EXISTS $_apiConfigsTable(api_config_id INTEGER PRIMARY KEY, config_json TEXT)'); await db.execute('CREATE TABLE IF NOT EXISTS $_ftpConfigsTable(ftp_config_id INTEGER PRIMARY KEY, config_json TEXT)'); } // --- ADDED: Upgrade logic for the new retry queue table --- if (oldVersion < 17) { await db.execute(''' CREATE TABLE IF NOT EXISTS $_retryQueueTable( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, endpoint_or_path TEXT NOT NULL, payload TEXT, timestamp TEXT NOT NULL, status TEXT NOT NULL ) '''); } } /// Performs an "upsert": inserts new records or replaces existing ones. Future _upsertData(String table, String idKeyName, List> data, String jsonKeyName) async { if (data.isEmpty) return; final db = await database; final batch = db.batch(); for (var item in data) { batch.insert( table, {idKeyName: item[idKeyName], '${jsonKeyName}_json': jsonEncode(item)}, conflictAlgorithm: ConflictAlgorithm.replace, ); } await batch.commit(noResult: true); debugPrint("Upserted ${data.length} items into $table"); } /// Deletes a list of records from a table by their primary keys. Future _deleteData(String table, String idKeyName, List ids) async { if (ids.isEmpty) return; final db = await database; final placeholders = List.filled(ids.length, '?').join(', '); await db.delete( table, where: '$idKeyName IN ($placeholders)', whereArgs: ids, ); debugPrint("Deleted ${ids.length} items from $table"); } Future>?> _loadData(String table, String jsonKey) async { final db = await database; final List> maps = await db.query(table); if (maps.isNotEmpty) { return maps.map((map) => jsonDecode(map['${jsonKey}_json']) as Map).toList(); } return null; } Future saveProfile(Map profile) async { final db = await database; await db.insert(_profileTable, {'user_id': profile['user_id'], 'profile_json': jsonEncode(profile)}, conflictAlgorithm: ConflictAlgorithm.replace); } Future?> loadProfile() async { final db = await database; final List> maps = await db.query(_profileTable); if(maps.isNotEmpty) return jsonDecode(maps.first['profile_json']); return null; } // --- Upsert/Delete/Load methods for all data types --- Future upsertUsers(List> data) => _upsertData(_usersTable, 'user_id', data, 'user'); Future deleteUsers(List ids) => _deleteData(_usersTable, 'user_id', ids); Future>?> loadUsers() => _loadData(_usersTable, 'user'); Future upsertTarballStations(List> data) => _upsertData(_tarballStationsTable, 'station_id', data, 'station'); Future deleteTarballStations(List ids) => _deleteData(_tarballStationsTable, 'station_id', ids); Future>?> loadTarballStations() => _loadData(_tarballStationsTable, 'station'); Future upsertManualStations(List> data) => _upsertData(_manualStationsTable, 'station_id', data, 'station'); Future deleteManualStations(List ids) => _deleteData(_manualStationsTable, 'station_id', ids); Future>?> loadManualStations() => _loadData(_manualStationsTable, 'station'); Future upsertRiverManualStations(List> data) => _upsertData(_riverManualStationsTable, 'station_id', data, 'station'); Future deleteRiverManualStations(List ids) => _deleteData(_riverManualStationsTable, 'station_id', ids); Future>?> loadRiverManualStations() => _loadData(_riverManualStationsTable, 'station'); Future upsertRiverTriennialStations(List> data) => _upsertData(_riverTriennialStationsTable, 'station_id', data, 'station'); Future deleteRiverTriennialStations(List ids) => _deleteData(_riverTriennialStationsTable, 'station_id', ids); Future>?> loadRiverTriennialStations() => _loadData(_riverTriennialStationsTable, 'station'); Future upsertTarballClassifications(List> data) => _upsertData(_tarballClassificationsTable, 'classification_id', data, 'classification'); Future deleteTarballClassifications(List ids) => _deleteData(_tarballClassificationsTable, 'classification_id', ids); Future>?> loadTarballClassifications() => _loadData(_tarballClassificationsTable, 'classification'); Future upsertDepartments(List> data) => _upsertData(_departmentsTable, 'department_id', data, 'department'); Future deleteDepartments(List ids) => _deleteData(_departmentsTable, 'department_id', ids); Future>?> loadDepartments() => _loadData(_departmentsTable, 'department'); Future upsertCompanies(List> data) => _upsertData(_companiesTable, 'company_id', data, 'company'); Future deleteCompanies(List ids) => _deleteData(_companiesTable, 'company_id', ids); Future>?> loadCompanies() => _loadData(_companiesTable, 'company'); Future upsertPositions(List> data) => _upsertData(_positionsTable, 'position_id', data, 'position'); Future deletePositions(List ids) => _deleteData(_positionsTable, 'position_id', ids); Future>?> loadPositions() => _loadData(_positionsTable, 'position'); Future upsertAirManualStations(List> data) => _upsertData(_airManualStationsTable, 'station_id', data, 'station'); Future deleteAirManualStations(List ids) => _deleteData(_airManualStationsTable, 'station_id', ids); Future>?> loadAirManualStations() => _loadData(_airManualStationsTable, 'station'); Future upsertAirClients(List> data) => _upsertData(_airClientsTable, 'client_id', data, 'client'); Future deleteAirClients(List ids) => _deleteData(_airClientsTable, 'client_id', ids); Future>?> loadAirClients() => _loadData(_airClientsTable, 'client'); Future upsertStates(List> data) => _upsertData(_statesTable, 'state_id', data, 'state'); Future deleteStates(List ids) => _deleteData(_statesTable, 'state_id', ids); Future>?> loadStates() => _loadData(_statesTable, 'state'); Future upsertAppSettings(List> data) => _upsertData(_appSettingsTable, 'setting_id', data, 'setting'); Future deleteAppSettings(List ids) => _deleteData(_appSettingsTable, 'setting_id', ids); Future>?> loadAppSettings() => _loadData(_appSettingsTable, 'setting'); Future upsertParameterLimits(List> data) => _upsertData(_parameterLimitsTable, 'param_autoid', data, 'limit'); Future deleteParameterLimits(List ids) => _deleteData(_parameterLimitsTable, 'param_autoid', ids); Future>?> loadParameterLimits() => _loadData(_parameterLimitsTable, 'limit'); // --- ADDED: Methods for independent API and FTP configurations --- Future upsertApiConfigs(List> data) => _upsertData(_apiConfigsTable, 'api_config_id', data, 'config'); Future deleteApiConfigs(List ids) => _deleteData(_apiConfigsTable, 'api_config_id', ids); Future>?> loadApiConfigs() => _loadData(_apiConfigsTable, 'config'); Future upsertFtpConfigs(List> data) => _upsertData(_ftpConfigsTable, 'ftp_config_id', data, 'config'); Future deleteFtpConfigs(List ids) => _deleteData(_ftpConfigsTable, 'ftp_config_id', ids); Future>?> loadFtpConfigs() => _loadData(_ftpConfigsTable, 'config'); // --- ADDED: Methods for the new retry queue --- Future queueFailedRequest(Map data) async { final db = await database; return await db.insert(_retryQueueTable, data, conflictAlgorithm: ConflictAlgorithm.replace); } Future>> getPendingRequests() async { final db = await database; return await db.query(_retryQueueTable, where: 'status = ?', whereArgs: ['pending']); } Future?> getRequestById(int id) async { final db = await database; final results = await db.query(_retryQueueTable, where: 'id = ?', whereArgs: [id]); return results.isNotEmpty ? results.first : null; } Future deleteRequestFromQueue(int id) async { final db = await database; await db.delete(_retryQueueTable, where: 'id = ?', whereArgs: [id]); } }