environment_monitoring_app/lib/services/api_service.dart

1316 lines
54 KiB
Dart

// 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/air_collection_data.dart';
import 'package:environment_monitoring_app/models/air_installation_data.dart';
import 'package:environment_monitoring_app/services/server_config_service.dart';
import 'package:environment_monitoring_app/models/marine_manual_pre_departure_checklist_data.dart';
import 'package:environment_monitoring_app/models/marine_manual_sonde_calibration_data.dart';
import 'package:environment_monitoring_app/models/marine_manual_equipment_maintenance_data.dart';
// =======================================================================
// Part 1: Unified API Service
// =======================================================================
class ApiService {
final BaseApiService _baseService = BaseApiService();
final DatabaseHelper dbHelper = DatabaseHelper();
final ServerConfigService _serverConfigService = ServerConfigService();
late final MarineApiService marine;
late final RiverApiService river;
late final AirApiService air;
static const String imageBaseUrl = 'https://mms-apiv4.pstw.com.my/';
ApiService({required TelegramService telegramService}) {
marine = MarineApiService(_baseService, telegramService, _serverConfigService, dbHelper);
river = RiverApiService(_baseService, telegramService, _serverConfigService, dbHelper);
air = AirApiService(_baseService, telegramService, _serverConfigService);
}
// --- Core API Methods ---
Future<Map<String, dynamic>> login(String email, String password) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.post(baseUrl, 'auth/login', {'email': email, 'password': password});
}
Future<Map<String, dynamic>> register({
required String username,
String? firstName,
String? lastName,
required String email,
required String password,
String? phoneNumber,
int? departmentId,
int? companyId,
int? positionId,
}) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
final Map<String, dynamic> 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(baseUrl, 'auth/register', body);
}
Future<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> data) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.post(baseUrl, endpoint, data);
}
Future<Map<String, dynamic>> getProfile() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'profile');
}
Future<Map<String, dynamic>> getAllUsers() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'users');
}
Future<Map<String, dynamic>> getAllDepartments() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'departments');
}
Future<Map<String, dynamic>> getAllCompanies() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'companies');
}
Future<Map<String, dynamic>> getAllPositions() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'positions');
}
Future<Map<String, dynamic>> getAllStates() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'states');
}
Future<Map<String, dynamic>> sendTelegramAlert({
required String chatId,
required String message,
}) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.post(baseUrl, 'marine/telegram-alert', { // Note: Endpoint might need generalization if used by other modules
'chat_id': chatId,
'message': message,
});
}
Future<File?> 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<Map<String, dynamic>> uploadProfilePicture(File imageFile) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.postMultipart(
baseUrl: baseUrl,
endpoint: 'profile/upload-picture',
fields: {},
files: {'profile_picture': imageFile});
}
Future<Map<String, dynamic>> 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;
}
Future<void> validateToken() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
await _baseService.get(baseUrl, 'profile');
}
Future<Map<String, dynamic>> _fetchDelta(String endpoint, String? lastSyncTimestamp) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
String url = endpoint;
if (lastSyncTimestamp != null) {
url += '?since=$lastSyncTimestamp';
}
return _baseService.get(baseUrl, url);
}
Future<Map<String, dynamic>> syncAllData({String? lastSyncTimestamp}) async {
debugPrint('ApiService: Starting DELTA data sync. Since: $lastSyncTimestamp');
try {
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);
}
},
'documents': {
'endpoint': 'documents',
'handler': (d, id) async {
await dbHelper.upsertDocuments(d);
await dbHelper.deleteDocuments(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);
}
},
// --- ADDED: River Investigative Stations Sync ---
'riverInvestigativeStations': {
// IMPORTANT: Make sure this endpoint matches your server's route
'endpoint': 'river/investigative-stations',
'handler': (d, id) async {
await dbHelper.upsertRiverInvestigativeStations(d);
await dbHelper.deleteRiverInvestigativeStations(id);
}
},
// --- END ADDED ---
'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);
}
},
'npeParameterLimits': {
'endpoint': 'npe-parameter-limits',
'handler': (d, id) async {
await dbHelper.upsertNpeParameterLimits(d);
await dbHelper.deleteNpeParameterLimits(id);
}
},
'marineParameterLimits': {
'endpoint': 'marine-parameter-limits',
'handler': (d, id) async {
await dbHelper.upsertMarineParameterLimits(d);
await dbHelper.deleteMarineParameterLimits(id);
}
},
'riverParameterLimits': {
'endpoint': 'river-parameter-limits',
'handler': (d, id) async {
await dbHelper.upsertRiverParameterLimits(d);
await dbHelper.deleteRiverParameterLimits(id);
}
},
'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) {
if (key == 'profile') {
// Handle potential non-list response for profile
final profileData = result['data'];
if (profileData is Map<String, dynamic>) {
await (syncTasks[key]!['handler'] as Function)([profileData], []);
} else if (profileData is List && profileData.isNotEmpty) {
await (syncTasks[key]!['handler'] as Function)([profileData.first], []);
}
} else {
final updated = List<Map<String, dynamic>>.from(result['data']['updated'] ?? []);
final deleted = List<dynamic>.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');
rethrow;
}
}
Future<Map<String, dynamic>> syncRegistrationData() async {
debugPrint('ApiService: Starting registration data sync...');
try {
final syncTasks = {
'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);
}
},
};
final fetchFutures = syncTasks.map((key, value) =>
MapEntry(key, _fetchDelta(value['endpoint'] as String, null))); // Fetch all data
final results = await Future.wait(fetchFutures.values);
final resultData = Map.fromIterables(fetchFutures.keys, results);
for (var entry in resultData.entries) {
final key = entry.key;
final result = entry.value;
// Assuming the full list is returned in 'data' when lastSyncTimestamp is null
if (result['success'] == true && result['data'] != null) {
// Ensure 'data' is treated as a list, even if API might sometimes return a map for single results
List<Map<String, dynamic>> allData = [];
if (result['data'] is List) {
allData = List<Map<String, dynamic>>.from(result['data']);
} else if (result['data'] is Map) {
// Handle cases where the API might return just a map if only one item exists
// Or if the structure is like {'data': [...]} incorrectly
var potentialList = (result['data'] as Map).values.firstWhere((v) => v is List, orElse: () => null);
if (potentialList != null) {
allData = List<Map<String, dynamic>>.from(potentialList);
} else {
debugPrint('ApiService: Unexpected data format for $key. Expected List, got Map.');
}
}
// Since it's a full sync, we just upsert everything and don't delete
if (allData.isNotEmpty) {
await (syncTasks[key]!['handler'] as Function)(allData, []);
} else if (result['data'] is Map && allData.isEmpty) {
// If it was a map and we couldn't extract a list, log it.
debugPrint('ApiService: Data for $key was a map, but could not extract list for handler.');
}
} else {
debugPrint('ApiService: Failed to sync $key. Message: ${result['message']}');
}
}
debugPrint('ApiService: Registration data sync complete.');
return {'success': true, 'message': 'Registration data sync successful.'};
} catch (e) {
debugPrint('ApiService: Registration data sync failed: $e');
return {'success': false, 'message': 'Registration data sync failed: $e'};
}
}
}
// =======================================================================
// Part 2: Feature-Specific API Services (Refactored to include Telegram)
// =======================================================================
class AirApiService {
final BaseApiService _baseService;
final TelegramService? _telegramService; // Kept optional for now
final ServerConfigService _serverConfigService;
AirApiService(this._baseService, this._telegramService, this._serverConfigService);
Future<Map<String, dynamic>> getManualStations() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'air/manual-stations');
}
Future<Map<String, dynamic>> getClients() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'air/clients');
}
// NOTE: Air submission logic is likely in AirSamplingService and might use generic services.
// These specific methods might be legacy or used differently. Keep them for now.
Future<Map<String, dynamic>> submitInstallation(AirInstallationData data) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.post(baseUrl, 'air/manual/installation', data.toJsonForApi());
}
Future<Map<String, dynamic>> submitCollection(AirCollectionData data) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.post(baseUrl, 'air/manual/collection', data.toJson());
}
Future<Map<String, dynamic>> uploadInstallationImages({
required String airManId,
required Map<String, File> files,
}) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.postMultipart(
baseUrl: baseUrl,
endpoint: 'air/manual/installation-images',
fields: {'air_man_id': airManId},
files: files,
);
}
Future<Map<String, dynamic>> uploadCollectionImages({
required String airManId,
required Map<String, File> files,
}) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.postMultipart(
baseUrl: baseUrl,
endpoint: 'air/manual/collection-images',
fields: {'air_man_id': airManId},
files: files,
);
}
}
// =======================================================================
// --- START OF MODIFIED SECTION ---
// The entire MarineApiService class is replaced with the corrected version.
// =======================================================================
class MarineApiService {
final BaseApiService _baseService;
final TelegramService _telegramService;
final ServerConfigService _serverConfigService;
final DatabaseHelper _dbHelper; // Kept to match constructor
MarineApiService(this._baseService, this._telegramService, this._serverConfigService, this._dbHelper);
// --- KEPT METHODS (Unchanged) ---
Future<Map<String, dynamic>> getTarballStations() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'marine/tarball/stations');
}
Future<Map<String, dynamic>> getManualStations() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'marine/manual/stations');
}
Future<Map<String, dynamic>> getTarballClassifications() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'marine/tarball/classifications');
}
// --- REPLACED/FIXED METHOD ---
Future<Map<String, dynamic>> getManualSamplingImages({
required int stationId,
required DateTime samplingDate,
required String samplingType, // This parameter is NOW USED
}) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
final String dateStr = DateFormat('yyyy-MM-dd').format(samplingDate);
String endpoint;
// Determine the correct endpoint based on the sampling type
switch (samplingType) {
case 'In-Situ Sampling':
endpoint = 'marine/manual/records-by-station?station_id=$stationId&date=$dateStr';
break;
case 'Tarball Sampling':
// **IMPORTANT**: Please verify this is the correct endpoint for tarball records
endpoint = 'marine/tarball/records-by-station?station_id=$stationId&date=$dateStr';
break;
case 'All Manual Sampling':
default:
// 'All' is complex. Defaulting to 'manual' (in-situ) as a fallback.
endpoint = 'marine/manual/records-by-station?station_id=$stationId&date=$dateStr';
break;
}
// This new debug print will help you confirm the fix is working
debugPrint("MarineApiService: Calling API endpoint: $endpoint");
final response = await _baseService.get(baseUrl, endpoint);
// Adjusting response parsing based on observed structure
if (response['success'] == true && response['data'] is Map && response['data']['data'] is List) {
return {
'success': true,
'data': response['data']['data'], // Return the inner 'data' list
'message': response['message'],
};
}
// Return original response if structure doesn't match
return response;
}
// --- ADDED METHOD ---
Future<Map<String, dynamic>> sendImageRequestEmail({
required String recipientEmail,
required List<String> imageUrls,
required String stationName,
required String samplingDate,
}) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
final Map<String, String> fields = {
'recipientEmail': recipientEmail,
'imageUrls': jsonEncode(imageUrls),
'stationName': stationName,
'samplingDate': samplingDate,
};
return _baseService.postMultipart(
baseUrl: baseUrl,
endpoint: 'marine/images/send-email', // **IMPORTANT**: Verify this endpoint
fields: fields,
files: {},
);
}
// --- KEPT METHODS (Unchanged) ---
Future<Map<String, dynamic>> submitPreDepartureChecklist(MarineManualPreDepartureChecklistData data) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.post(baseUrl, 'marine/checklist', data.toApiFormData());
}
Future<Map<String, dynamic>> submitSondeCalibration(MarineManualSondeCalibrationData data) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.post(baseUrl, 'marine/calibration', data.toApiFormData());
}
Future<Map<String, dynamic>> submitMaintenanceLog(MarineManualEquipmentMaintenanceData data) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.post(baseUrl, 'marine/maintenance', data.toApiFormData());
}
Future<Map<String, dynamic>> getPreviousMaintenanceLogs() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'marine/maintenance/previous');
}
}
// =======================================================================
// --- END OF MODIFIED SECTION ---
// =======================================================================
class RiverApiService {
final BaseApiService _baseService;
final TelegramService _telegramService; // Still needed if _handleAlerts were here
final ServerConfigService _serverConfigService;
final DatabaseHelper _dbHelper; // Still needed for parameter limit lookups if alerts were here
RiverApiService(this._baseService, this._telegramService, this._serverConfigService, this._dbHelper);
// --- KEPT METHODS ---
Future<Map<String, dynamic>> getManualStations() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'river/manual-stations');
}
Future<Map<String, dynamic>> getTriennialStations() async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
return _baseService.get(baseUrl, 'river/triennial-stations');
}
Future<Map<String, dynamic>> getRiverSamplingImages({
required int stationId,
required DateTime samplingDate,
required String samplingType, // Parameter likely unused by current endpoint
}) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
final String dateStr = DateFormat('yyyy-MM-dd').format(samplingDate);
// Endpoint seems specific to 'manual', adjust if needed for 'triennial' or others
final String endpoint = 'river/manual/images-by-station?station_id=$stationId&date=$dateStr';
debugPrint("ApiService: Calling river image request API endpoint: $endpoint");
final response = await _baseService.get(baseUrl, endpoint);
return response; // Pass the raw response along
}
Future<Map<String, dynamic>> sendImageRequestEmail({
required String recipientEmail,
required List<String> imageUrls,
required String stationName,
required String samplingDate,
}) async {
final baseUrl = await _serverConfigService.getActiveApiUrl();
final Map<String, String> fields = {
'recipientEmail': recipientEmail,
'imageUrls': jsonEncode(imageUrls),
'stationName': stationName,
'samplingDate': samplingDate,
};
return _baseService.postMultipart(
baseUrl: baseUrl,
endpoint: 'river/images/send-email', // Endpoint for river email requests
fields: fields,
files: {},
);
}
// --- REMOVED METHODS (Logic moved to feature services) ---
// - submitInSituSample
// - submitTriennialSample
// - _handleTriennialSuccessAlert
// - _handleInSituSuccessAlert
// - _generateInSituAlertMessage
// - _getOutOfBoundsAlertSection (River version)
}
// =======================================================================
// Part 3: Local Database Helper (Original version - no compute mods)
// =======================================================================
class DatabaseHelper {
static Database? _database;
static const String _dbName = 'app_data.db';
// --- MODIFIED: Incremented DB version ---
static const int _dbVersion = 24; // Keep version updated if schema changes
// --- END MODIFIED ---
// compute-related static variables/methods REMOVED
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';
// --- ADDED: River Investigative Stations Table Name ---
static const String _riverInvestigativeStationsTable = 'river_investigative_stations';
// --- END ADDED ---
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';
static const String _appSettingsTable = 'app_settings';
// static const String _parameterLimitsTable = 'manual_parameter_limits'; // REMOVED
static const String _npeParameterLimitsTable = 'npe_parameter_limits';
static const String _marineParameterLimitsTable = 'marine_parameter_limits';
static const String _riverParameterLimitsTable = 'river_parameter_limits';
static const String _apiConfigsTable = 'api_configurations';
static const String _ftpConfigsTable = 'ftp_configurations';
static const String _retryQueueTable = 'retry_queue';
static const String _submissionLogTable = 'submission_log';
static const String _documentsTable = 'documents';
static const String _modulePreferencesTable = 'module_preferences';
static const String _moduleApiLinksTable = 'module_api_links';
static const String _moduleFtpLinksTable = 'module_ftp_links';
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB();
return _database!;
}
Future<Database> _initDB() async {
// Standard path retrieval
String dbPath = p.join(await getDatabasesPath(), _dbName);
return await openDatabase(dbPath, version: _dbVersion, onCreate: _onCreate, onUpgrade: _onUpgrade);
}
Future _onCreate(Database db, int version) async {
// Create all tables as defined in version 23
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,
email TEXT UNIQUE,
password_hash TEXT,
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)');
// --- ADDED: River Investigative Stations Table Create ---
await db.execute('CREATE TABLE $_riverInvestigativeStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)');
// --- END ADDED ---
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)');
await db.execute('CREATE TABLE $_appSettingsTable(setting_id INTEGER PRIMARY KEY, setting_json TEXT)');
// No generic _parameterLimitsTable creation
await db.execute('CREATE TABLE $_npeParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
await db.execute('CREATE TABLE $_marineParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
await db.execute('CREATE TABLE $_riverParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
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)');
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
)
''');
await db.execute('''
CREATE TABLE $_submissionLogTable (
submission_id TEXT PRIMARY KEY,
module TEXT NOT NULL,
type TEXT NOT NULL,
status TEXT NOT NULL,
message TEXT,
report_id TEXT,
created_at TEXT NOT NULL,
form_data TEXT,
image_data TEXT,
server_name TEXT,
api_status TEXT,
ftp_status TEXT
)
''');
await db.execute('''
CREATE TABLE $_modulePreferencesTable (
module_name TEXT PRIMARY KEY,
is_api_enabled INTEGER NOT NULL DEFAULT 1,
is_ftp_enabled INTEGER NOT NULL DEFAULT 1
)
''');
await db.execute('''
CREATE TABLE $_moduleApiLinksTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
module_name TEXT NOT NULL,
api_config_id INTEGER NOT NULL,
is_enabled INTEGER NOT NULL DEFAULT 1
)
''');
await db.execute('''
CREATE TABLE $_moduleFtpLinksTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
module_name TEXT NOT NULL,
ftp_config_id INTEGER NOT NULL,
is_enabled INTEGER NOT NULL DEFAULT 1
)
''');
await db.execute('CREATE TABLE $_documentsTable(id INTEGER PRIMARY KEY, document_json TEXT)');
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
// Apply upgrades sequentially
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)');
}
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)');
}
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
)
''');
}
if (oldVersion < 18) {
await db.execute('''
CREATE TABLE IF NOT EXISTS $_submissionLogTable (
submission_id TEXT PRIMARY KEY,
module TEXT NOT NULL,
type TEXT NOT NULL,
status TEXT NOT NULL,
message TEXT,
report_id TEXT,
created_at TEXT NOT NULL,
form_data TEXT,
image_data TEXT,
server_name TEXT
)
''');
}
if (oldVersion < 19) {
try {
await db.execute("ALTER TABLE $_submissionLogTable ADD COLUMN api_status TEXT");
await db.execute("ALTER TABLE $_submissionLogTable ADD COLUMN ftp_status TEXT");
} catch (_) {}
await db.execute('''
CREATE TABLE IF NOT EXISTS $_modulePreferencesTable (
module_name TEXT PRIMARY KEY,
is_api_enabled INTEGER NOT NULL DEFAULT 1,
is_ftp_enabled INTEGER NOT NULL DEFAULT 1
)
''');
await db.execute('''
CREATE TABLE IF NOT EXISTS $_moduleApiLinksTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
module_name TEXT NOT NULL,
api_config_id INTEGER NOT NULL,
is_enabled INTEGER NOT NULL DEFAULT 1
)
''');
await db.execute('''
CREATE TABLE IF NOT EXISTS $_moduleFtpLinksTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
module_name TEXT NOT NULL,
ftp_config_id INTEGER NOT NULL,
is_enabled INTEGER NOT NULL DEFAULT 1
)
''');
}
if (oldVersion < 20) {
await db.execute('CREATE TABLE IF NOT EXISTS $_documentsTable(id INTEGER PRIMARY KEY, document_json TEXT)');
}
if (oldVersion < 21) {
try {
await db.execute("ALTER TABLE $_usersTable ADD COLUMN email TEXT");
await db.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_user_email ON $_usersTable (email)");
} catch (e) {
debugPrint("Upgrade warning: Failed to add email column/index to users table (may already exist): $e");
}
try {
await db.execute("ALTER TABLE $_usersTable ADD COLUMN password_hash TEXT");
} catch (e) {
debugPrint("Upgrade warning: Failed to add password_hash column to users table (may already exist): $e");
}
}
if (oldVersion < 23) {
await db.execute('CREATE TABLE IF NOT EXISTS $_npeParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
await db.execute('CREATE TABLE IF NOT EXISTS $_marineParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
await db.execute('CREATE TABLE IF NOT EXISTS $_riverParameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)');
try {
// await db.execute('DROP TABLE IF EXISTS $_parameterLimitsTable'); // Keep commented
debugPrint("Old generic parameter limits table check/drop logic executed (if applicable).");
} catch (e) {
debugPrint("Upgrade warning: Failed to drop old parameter limits table (may not exist): $e");
}
}
// --- ADDED: Upgrade step for new table ---
if (oldVersion < 24) {
await db.execute('CREATE TABLE IF NOT EXISTS $_riverInvestigativeStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)');
}
// --- END ADDED ---
}
// --- Data Handling Methods ---
Future<void> _upsertData(String table, String idKeyName, List<Map<String, dynamic>> data, String jsonKeyName) async {
if (data.isEmpty) return;
final db = await database;
final batch = db.batch();
for (var item in data) {
if (item[idKeyName] != null) {
batch.insert(
table,
{idKeyName: item[idKeyName], '${jsonKeyName}_json': jsonEncode(item)},
conflictAlgorithm: ConflictAlgorithm.replace,
);
} else {
debugPrint("Skipping upsert for item in $table due to null ID: $item");
}
}
await batch.commit(noResult: true);
debugPrint("Upserted items into $table (skipped items with null IDs if any)");
}
Future<void> _deleteData(String table, String idKeyName, List<dynamic> ids) async {
if (ids.isEmpty) return;
final db = await database;
final validIds = ids.where((id) => id != null).toList();
if (validIds.isEmpty) return;
final placeholders = List.filled(validIds.length, '?').join(', ');
await db.delete(
table,
where: '$idKeyName IN ($placeholders)',
whereArgs: validIds,
);
debugPrint("Deleted ${validIds.length} items from $table");
}
Future<List<Map<String, dynamic>>?> _loadData(String table, String jsonKey) async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(table);
if (maps.isNotEmpty) {
try {
return maps.map((map) {
try {
return jsonDecode(map['${jsonKey}_json']) as Map<String, dynamic>;
} catch (e) {
final idKey = maps.first.keys.firstWhere((k) => k.endsWith('_id') || k == 'id' || k.endsWith('autoid'), orElse: () => 'unknown_id');
debugPrint("Error decoding JSON from $table, ID ${map[idKey]}: $e");
return <String, dynamic>{};
}
}).where((item) => item.isNotEmpty).toList();
} catch (e) {
debugPrint("General error loading data from $table: $e");
return null;
}
}
return null; // Return null if table is empty
}
Future<void> saveProfile(Map<String, dynamic> profile) async {
final db = await database;
await db.insert(_profileTable, {'user_id': profile['user_id'], 'profile_json': jsonEncode(profile)},
conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<Map<String, dynamic>?> loadProfile() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(_profileTable);
if (maps.isNotEmpty) {
try {
return jsonDecode(maps.first['profile_json']);
} catch (e) {
debugPrint("Error decoding profile: $e");
return null;
}
}
return null;
}
Future<Map<String, dynamic>?> loadProfileByEmail(String email) async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(
_usersTable,
columns: ['user_json'],
where: 'email = ?',
whereArgs: [email],
);
if (maps.isNotEmpty) {
try {
return jsonDecode(maps.first['user_json']) as Map<String, dynamic>;
} catch (e) {
debugPrint("Error decoding profile for email $email: $e");
return null;
}
}
return null;
}
Future<void> upsertUserWithCredentials({
required Map<String, dynamic> profile,
required String passwordHash,
}) async {
final db = await database;
await db.insert(
_usersTable,
{
'user_id': profile['user_id'],
'email': profile['email'],
'password_hash': passwordHash,
'user_json': jsonEncode(profile)
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
debugPrint("Upserted user credentials for ${profile['email']}");
}
Future<String?> getUserPasswordHashByEmail(String email) async {
final db = await database;
final List<Map<String, dynamic>> result = await db.query(
_usersTable,
columns: ['password_hash'],
where: 'email = ?',
whereArgs: [email],
);
if (result.isNotEmpty && result.first['password_hash'] != null) {
return result.first['password_hash'] as String;
}
return null;
}
Future<void> upsertUsers(List<Map<String, dynamic>> data) async {
if (data.isEmpty) return;
final db = await database;
final batch = db.batch();
for (var item in data) {
String email = item['email'] ?? 'missing_email_${item['user_id']}@placeholder.com';
if (item['email'] == null) {
debugPrint("Warning: User ID ${item['user_id']} is missing email during upsert.");
}
batch.insert(
_usersTable,
{
'user_id': item['user_id'],
'email': email,
'user_json': jsonEncode(item),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
debugPrint("Upserted ${data.length} user items using batch.");
}
Future<void> deleteUsers(List<dynamic> ids) => _deleteData(_usersTable, 'user_id', ids);
Future<List<Map<String, dynamic>>?> loadUsers() => _loadData(_usersTable, 'user');
Future<void> upsertDocuments(List<Map<String, dynamic>> data) => _upsertData(_documentsTable, 'id', data, 'document');
Future<void> deleteDocuments(List<dynamic> ids) => _deleteData(_documentsTable, 'id', ids);
Future<List<Map<String, dynamic>>?> loadDocuments() => _loadData(_documentsTable, 'document');
Future<void> upsertTarballStations(List<Map<String, dynamic>> data) =>
_upsertData(_tarballStationsTable, 'station_id', data, 'station');
Future<void> deleteTarballStations(List<dynamic> ids) => _deleteData(_tarballStationsTable, 'station_id', ids);
Future<List<Map<String, dynamic>>?> loadTarballStations() => _loadData(_tarballStationsTable, 'station');
Future<void> upsertManualStations(List<Map<String, dynamic>> data) =>
_upsertData(_manualStationsTable, 'station_id', data, 'station');
Future<void> deleteManualStations(List<dynamic> ids) => _deleteData(_manualStationsTable, 'station_id', ids);
Future<List<Map<String, dynamic>>?> loadManualStations() => _loadData(_manualStationsTable, 'station');
Future<void> upsertRiverManualStations(List<Map<String, dynamic>> data) =>
_upsertData(_riverManualStationsTable, 'station_id', data, 'station');
Future<void> deleteRiverManualStations(List<dynamic> ids) => _deleteData(_riverManualStationsTable, 'station_id', ids);
Future<List<Map<String, dynamic>>?> loadRiverManualStations() => _loadData(_riverManualStationsTable, 'station');
Future<void> upsertRiverTriennialStations(List<Map<String, dynamic>> data) =>
_upsertData(_riverTriennialStationsTable, 'station_id', data, 'station');
Future<void> deleteRiverTriennialStations(List<dynamic> ids) => _deleteData(_riverTriennialStationsTable, 'station_id', ids);
Future<List<Map<String, dynamic>>?> loadRiverTriennialStations() => _loadData(_riverTriennialStationsTable, 'station');
// --- ADDED: River Investigative Stations DB Methods ---
Future<void> upsertRiverInvestigativeStations(List<Map<String, dynamic>> data) =>
_upsertData(_riverInvestigativeStationsTable, 'station_id', data, 'station');
Future<void> deleteRiverInvestigativeStations(List<dynamic> ids) => _deleteData(_riverInvestigativeStationsTable, 'station_id', ids);
Future<List<Map<String, dynamic>>?> loadRiverInvestigativeStations() => _loadData(_riverInvestigativeStationsTable, 'station');
// --- END ADDED ---
Future<void> upsertTarballClassifications(List<Map<String, dynamic>> data) =>
_upsertData(_tarballClassificationsTable, 'classification_id', data, 'classification');
Future<void> deleteTarballClassifications(List<dynamic> ids) => _deleteData(_tarballClassificationsTable, 'classification_id', ids);
Future<List<Map<String, dynamic>>?> loadTarballClassifications() => _loadData(_tarballClassificationsTable, 'classification');
Future<void> upsertDepartments(List<Map<String, dynamic>> data) => _upsertData(_departmentsTable, 'department_id', data, 'department');
Future<void> deleteDepartments(List<dynamic> ids) => _deleteData(_departmentsTable, 'department_id', ids);
Future<List<Map<String, dynamic>>?> loadDepartments() => _loadData(_departmentsTable, 'department');
Future<void> upsertCompanies(List<Map<String, dynamic>> data) => _upsertData(_companiesTable, 'company_id', data, 'company');
Future<void> deleteCompanies(List<dynamic> ids) => _deleteData(_companiesTable, 'company_id', ids);
Future<List<Map<String, dynamic>>?> loadCompanies() => _loadData(_companiesTable, 'company');
Future<void> upsertPositions(List<Map<String, dynamic>> data) => _upsertData(_positionsTable, 'position_id', data, 'position');
Future<void> deletePositions(List<dynamic> ids) => _deleteData(_positionsTable, 'position_id', ids);
Future<List<Map<String, dynamic>>?> loadPositions() => _loadData(_positionsTable, 'position');
Future<void> upsertAirManualStations(List<Map<String, dynamic>> data) =>
_upsertData(_airManualStationsTable, 'station_id', data, 'station');
Future<void> deleteAirManualStations(List<dynamic> ids) => _deleteData(_airManualStationsTable, 'station_id', ids);
Future<List<Map<String, dynamic>>?> loadAirManualStations() => _loadData(_airManualStationsTable, 'station');
Future<void> upsertAirClients(List<Map<String, dynamic>> data) => _upsertData(_airClientsTable, 'client_id', data, 'client');
Future<void> deleteAirClients(List<dynamic> ids) => _deleteData(_airClientsTable, 'client_id', ids);
Future<List<Map<String, dynamic>>?> loadAirClients() => _loadData(_airClientsTable, 'client');
Future<void> upsertStates(List<Map<String, dynamic>> data) => _upsertData(_statesTable, 'state_id', data, 'state');
Future<void> deleteStates(List<dynamic> ids) => _deleteData(_statesTable, 'state_id', ids);
Future<List<Map<String, dynamic>>?> loadStates() => _loadData(_statesTable, 'state');
Future<void> upsertAppSettings(List<Map<String, dynamic>> data) => _upsertData(_appSettingsTable, 'setting_id', data, 'setting');
Future<void> deleteAppSettings(List<dynamic> ids) => _deleteData(_appSettingsTable, 'setting_id', ids);
Future<List<Map<String, dynamic>>?> loadAppSettings() => _loadData(_appSettingsTable, 'setting');
Future<void> upsertNpeParameterLimits(List<Map<String, dynamic>> data) => _upsertData(_npeParameterLimitsTable, 'param_autoid', data, 'limit');
Future<void> deleteNpeParameterLimits(List<dynamic> ids) => _deleteData(_npeParameterLimitsTable, 'param_autoid', ids);
Future<List<Map<String, dynamic>>?> loadNpeParameterLimits() => _loadData(_npeParameterLimitsTable, 'limit');
Future<void> upsertMarineParameterLimits(List<Map<String, dynamic>> data) => _upsertData(_marineParameterLimitsTable, 'param_autoid', data, 'limit');
Future<void> deleteMarineParameterLimits(List<dynamic> ids) => _deleteData(_marineParameterLimitsTable, 'param_autoid', ids);
Future<List<Map<String, dynamic>>?> loadMarineParameterLimits() => _loadData(_marineParameterLimitsTable, 'limit');
Future<void> upsertRiverParameterLimits(List<Map<String, dynamic>> data) => _upsertData(_riverParameterLimitsTable, 'param_autoid', data, 'limit');
Future<void> deleteRiverParameterLimits(List<dynamic> ids) => _deleteData(_riverParameterLimitsTable, 'param_autoid', ids);
Future<List<Map<String, dynamic>>?> loadRiverParameterLimits() => _loadData(_riverParameterLimitsTable, 'limit');
Future<void> upsertApiConfigs(List<Map<String, dynamic>> data) => _upsertData(_apiConfigsTable, 'api_config_id', data, 'config');
Future<void> deleteApiConfigs(List<dynamic> ids) => _deleteData(_apiConfigsTable, 'api_config_id', ids);
Future<List<Map<String, dynamic>>?> loadApiConfigs() => _loadData(_apiConfigsTable, 'config');
Future<void> upsertFtpConfigs(List<Map<String, dynamic>> data) => _upsertData(_ftpConfigsTable, 'ftp_config_id', data, 'config');
Future<void> deleteFtpConfigs(List<dynamic> ids) => _deleteData(_ftpConfigsTable, 'ftp_config_id', ids);
Future<List<Map<String, dynamic>>?> loadFtpConfigs() => _loadData(_ftpConfigsTable, 'config');
Future<int> queueFailedRequest(Map<String, dynamic> data) async {
final db = await database;
return await db.insert(_retryQueueTable, data, conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<List<Map<String, dynamic>>> getPendingRequests() async {
final db = await database;
return await db.query(_retryQueueTable, where: 'status = ?', whereArgs: ['pending'], orderBy: 'timestamp ASC'); // Order by timestamp
}
Future<Map<String, dynamic>?> 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<void> deleteRequestFromQueue(int id) async {
final db = await database;
await db.delete(_retryQueueTable, where: 'id = ?', whereArgs: [id]);
}
Future<void> saveSubmissionLog(Map<String, dynamic> data) async {
final db = await database;
await db.insert(
_submissionLogTable,
data,
conflictAlgorithm: ConflictAlgorithm.replace, // Replace if same ID exists
);
}
Future<List<Map<String, dynamic>>?> loadSubmissionLogs({String? module}) async {
final db = await database;
List<Map<String, dynamic>> maps;
try { // Add try-catch for robustness
if (module != null && module.isNotEmpty) {
maps = await db.query(
_submissionLogTable,
where: 'module = ?',
whereArgs: [module],
orderBy: 'created_at DESC',
);
} else {
maps = await db.query(
_submissionLogTable,
orderBy: 'created_at DESC',
);
}
return maps.isNotEmpty ? maps : null; // Return null if empty
} catch (e) {
debugPrint("Error loading submission logs: $e");
return null;
}
}
Future<void> saveModulePreference({
required String moduleName,
required bool isApiEnabled,
required bool isFtpEnabled,
}) async {
final db = await database;
await db.insert(
_modulePreferencesTable,
{
'module_name': moduleName,
'is_api_enabled': isApiEnabled ? 1 : 0,
'is_ftp_enabled': isFtpEnabled ? 1 : 0,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<Map<String, dynamic>?> getModulePreference(String moduleName) async {
final db = await database;
final result = await db.query(
_modulePreferencesTable,
where: 'module_name = ?',
whereArgs: [moduleName],
);
if (result.isNotEmpty) {
final row = result.first;
return {
'module_name': row['module_name'],
'is_api_enabled': (row['is_api_enabled'] as int) == 1,
'is_ftp_enabled': (row['is_ftp_enabled'] as int) == 1,
};
}
// Return default values if no preference found
return {'module_name': moduleName, 'is_api_enabled': true, 'is_ftp_enabled': true};
}
Future<void> saveApiLinksForModule(String moduleName, List<Map<String, dynamic>> links) async {
final db = await database;
await db.transaction((txn) async {
await txn.delete(_moduleApiLinksTable, where: 'module_name = ?', whereArgs: [moduleName]);
for (final link in links) {
if (link['api_config_id'] != null) { // Ensure ID is not null
await txn.insert(_moduleApiLinksTable, {
'module_name': moduleName,
'api_config_id': link['api_config_id'],
'is_enabled': (link['is_enabled'] as bool? ?? true) ? 1 : 0,
});
}
}
});
}
Future<void> saveFtpLinksForModule(String moduleName, List<Map<String, dynamic>> links) async {
final db = await database;
await db.transaction((txn) async {
await txn.delete(_moduleFtpLinksTable, where: 'module_name = ?', whereArgs: [moduleName]);
for (final link in links) {
if (link['ftp_config_id'] != null) { // Ensure ID is not null
await txn.insert(_moduleFtpLinksTable, {
'module_name': moduleName,
'ftp_config_id': link['ftp_config_id'],
'is_enabled': (link['is_enabled'] as bool? ?? true) ? 1 : 0,
});
}
}
});
}
Future<List<Map<String, dynamic>>> getAllApiLinksForModule(String moduleName) async {
final db = await database;
final result = await db.query(_moduleApiLinksTable, where: 'module_name = ?', whereArgs: [moduleName]);
return result.map((row) => {
'api_config_id': row['api_config_id'],
'is_enabled': (row['is_enabled'] as int) == 1,
}).toList();
}
Future<List<Map<String, dynamic>>> getAllFtpLinksForModule(String moduleName) async {
final db = await database;
final result = await db.query(_moduleFtpLinksTable, where: 'module_name = ?', whereArgs: [moduleName]);
return result.map((row) => {
'ftp_config_id': row['ftp_config_id'],
'is_enabled': (row['is_enabled'] as int) == 1,
}).toList();
}
}