// lib/services/local_storage_service.dart import 'dart:io'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:path/path.dart' as p; import 'package:dio/dio.dart'; import 'package:geolocator/geolocator.dart'; import '../models/air_installation_data.dart'; import '../models/air_collection_data.dart'; import '../models/tarball_data.dart'; import '../models/in_situ_sampling_data.dart'; import '../models/marine_manual_npe_report_data.dart'; // --- ADDED: Imports for new data models --- import '../models/marine_manual_pre_departure_checklist_data.dart'; import '../models/marine_manual_sonde_calibration_data.dart'; import '../models/marine_manual_equipment_maintenance_data.dart'; // --- END ADDED --- import '../models/river_in_situ_sampling_data.dart'; import '../models/river_manual_triennial_sampling_data.dart'; // --- ADDED IMPORT --- import '../models/marine_inves_manual_sampling_data.dart'; // --- ADDED IMPORT FOR RIVER INVESTIGATIVE --- import '../models/river_inves_manual_sampling_data.dart'; // --- END ADDED IMPORT --- class LocalStorageService { // ======================================================================= // Part 1: Public Storage Setup // ======================================================================= Future _requestPermissions() async { var status = await Permission.manageExternalStorage.request(); return status.isGranted; } Future _getPublicMMSV4Directory({required String serverName}) async { if (await _requestPermissions()) { final Directory? externalDir = await getExternalStorageDirectory(); if (externalDir != null) { final publicRootPath = externalDir.path.split('/Android/')[0]; final mmsv4Dir = Directory(p.join(publicRootPath, 'MMSV4', serverName)); if (!await mmsv4Dir.exists()) { await mmsv4Dir.create(recursive: true); } return mmsv4Dir; } } debugPrint("LocalStorageService: Manage External Storage permission was not granted."); return null; } Future getLogDirectory({required String serverName, required String module, required String subModule}) async { final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName); if (mmsv4Dir == null) return null; final logDir = Directory(p.join(mmsv4Dir.path, module, subModule)); if (!await logDir.exists()) { await logDir.create(recursive: true); } return logDir; } // ======================================================================= // Part 2: Air Manual Sampling Methods (LOGGING RESTORED) // ======================================================================= Future _getAirManualBaseDir({required String serverName}) async { final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName); if (mmsv4Dir == null) return null; final airDir = Directory(p.join(mmsv4Dir.path, 'air', 'air_manual_sampling')); if (!await airDir.exists()) { await airDir.create(recursive: true); } return airDir; } Future saveAirSamplingRecord(Map data, String refID, {required String serverName}) async { final baseDir = await _getAirManualBaseDir(serverName: serverName); if (baseDir == null) { debugPrint("Could not get public storage directory for Air Manual. Check permissions."); return null; } try { final eventDir = Directory(p.join(baseDir.path, refID)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } Future copyImageToLocal(dynamic imageFile) async { if (imageFile is! File) return null; try { if (p.dirname(imageFile.path) == eventDir.path) { return imageFile.path; } final String fileName = p.basename(imageFile.path); final File newFile = await imageFile.copy(p.join(eventDir.path, fileName)); return newFile.path; } catch (e) { debugPrint("Error copying file ${imageFile.path}: $e"); return null; } } final Map serializableData = Map.from(data); serializableData['serverConfigName'] = serverName; final installationImageKeys = ['imageFront', 'imageBack', 'imageLeft', 'imageRight', 'optionalImage1', 'optionalImage2', 'optionalImage3', 'optionalImage4']; for (final key in installationImageKeys) { if (serializableData.containsKey(key) && serializableData[key] is File) { final newPath = await copyImageToLocal(serializableData[key]); serializableData['${key}Path'] = newPath; } } if (serializableData['collectionData'] is Map) { final collectionMap = Map.from(serializableData['collectionData']); final collectionImageKeys = ['imageFront', 'imageBack', 'imageLeft', 'imageRight', 'imageChart', 'imageFilterPaper', 'optionalImage1', 'optionalImage2', 'optionalImage3', 'optionalImage4']; for (final key in collectionImageKeys) { if (collectionMap.containsKey(key) && collectionMap[key] is File) { final newPath = await copyImageToLocal(collectionMap[key]); collectionMap['${key}Path'] = newPath; } } serializableData['collectionData'] = collectionMap; } final Map finalData = Map.from(serializableData); void cleanMap(Map map) { map.removeWhere((key, value) => value is File); map.forEach((key, value) { if (value is Map) cleanMap(value as Map); }); } cleanMap(finalData); final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(finalData)); debugPrint("Air sampling log and images saved to: ${eventDir.path}"); return eventDir.path; } catch (e, s) { debugPrint("Error saving air sampling log to local storage: $e"); debugPrint("Stack trace: $s"); return null; } } Future>> getAllAirSamplingLogs() async { final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); if (mmsv4Root == null || !await mmsv4Root.exists()) return []; final List> allLogs = []; final serverDirs = mmsv4Root.listSync().whereType(); for (var serverDir in serverDirs) { final baseDir = Directory(p.join(serverDir.path, 'air', 'air_manual_sampling')); if (!await baseDir.exists()) continue; try { final entities = baseDir.listSync(); for (var entity in entities) { if (entity is Directory) { final jsonFile = File(p.join(entity.path, 'data.json')); if (await jsonFile.exists()) { final content = await jsonFile.readAsString(); final data = jsonDecode(content) as Map; data['logDirectory'] = entity.path; allLogs.add(data); } } } } catch (e) { debugPrint("Error reading air logs from ${baseDir.path}: $e"); } } return allLogs; } // ======================================================================= // Part 3: Tarball Specific Methods (LOGGING RESTORED) // ======================================================================= Future _getTarballBaseDir({required String serverName}) async { final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName); if (mmsv4Dir == null) return null; final tarballDir = Directory(p.join(mmsv4Dir.path, 'marine', 'marine_tarball_sampling')); if (!await tarballDir.exists()) { await tarballDir.create(recursive: true); } return tarballDir; } Future saveTarballSamplingData(TarballSamplingData data, {required String serverName}) async { final baseDir = await _getTarballBaseDir(serverName: serverName); if (baseDir == null) { debugPrint("Could not get public storage directory. Check permissions."); return null; } try { final stationCode = data.selectedStation?['tbl_station_code'] ?? 'UNKNOWN_STATION'; final timestamp = "${data.samplingDate}_${data.samplingTime?.replaceAll(':', '-')}"; final eventFolderName = "${stationCode}_$timestamp"; final eventDir = Directory(p.join(baseDir.path, eventFolderName)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } final Map jsonData = { ...data.toFormData(), 'submissionStatus': data.submissionStatus, 'submissionMessage': data.submissionMessage, 'reportId': data.reportId }; jsonData['serverConfigName'] = serverName; jsonData['selectedStation'] = data.selectedStation; jsonData['selectedClassification'] = data.selectedClassification; jsonData['secondSampler'] = data.secondSampler; final imageFiles = data.toImageFiles(); for (var entry in imageFiles.entries) { final File? imageFile = entry.value; if (imageFile != null && imageFile.path.isNotEmpty) { try { if (p.dirname(imageFile.path) == eventDir.path) { jsonData[entry.key] = imageFile.path; } else { final String originalFileName = p.basename(imageFile.path); final File newFile = await imageFile.copy(p.join(eventDir.path, originalFileName)); jsonData[entry.key] = newFile.path; } } catch (e) { debugPrint("Error processing image file ${imageFile.path}: $e"); jsonData[entry.key] = null; } } } final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(jsonData)); debugPrint("Tarball log saved to: ${jsonFile.path}"); return eventDir.path; } catch (e) { debugPrint("Error saving tarball log to local storage: $e"); return null; } } Future>> getAllTarballLogs() async { final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); if (mmsv4Root == null || !await mmsv4Root.exists()) return []; final List> allLogs = []; final serverDirs = mmsv4Root.listSync().whereType(); for (var serverDir in serverDirs) { final baseDir = Directory(p.join(serverDir.path, 'marine', 'marine_tarball_sampling')); if (!await baseDir.exists()) continue; try { final entities = baseDir.listSync(); for (var entity in entities) { if (entity is Directory) { final jsonFile = File(p.join(entity.path, 'data.json')); if (await jsonFile.exists()) { final content = await jsonFile.readAsString(); final data = jsonDecode(content) as Map; data['logDirectory'] = entity.path; allLogs.add(data); } } } } catch (e) { debugPrint("Error reading tarball logs from ${baseDir.path}: $e"); } } return allLogs; } Future updateTarballLog(Map updatedLogData) async { final logDir = updatedLogData['logDirectory']; if (logDir == null) { debugPrint("Cannot update log: logDirectory key is missing."); return; } try { final jsonFile = File(p.join(logDir, 'data.json')); if (await jsonFile.exists()) { updatedLogData.remove('isResubmitting'); await jsonFile.writeAsString(jsonEncode(updatedLogData)); debugPrint("Log updated successfully at: ${jsonFile.path}"); } } catch (e) { debugPrint("Error updating tarball log: $e"); } } // ======================================================================= // Part 4: Marine In-Situ Specific Methods (LOGGING RESTORED) // ======================================================================= Future getInSituBaseDir({required String serverName}) async { final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName); if (mmsv4Dir == null) return null; final inSituDir = Directory(p.join(mmsv4Dir.path, 'marine', 'marine_in_situ_sampling')); if (!await inSituDir.exists()) { await inSituDir.create(recursive: true); } return inSituDir; } Future saveInSituSamplingData(InSituSamplingData data, {required String serverName}) async { final baseDir = await getInSituBaseDir(serverName: serverName); if (baseDir == null) { debugPrint("Could not get public storage directory for In-Situ. Check permissions."); return null; } try { final stationCode = data.selectedStation?['man_station_code'] ?? 'UNKNOWN_STATION'; final timestamp = "${data.samplingDate}_${data.samplingTime?.replaceAll(':', '-')}"; final eventFolderName = "${stationCode}_$timestamp"; final eventDir = Directory(p.join(baseDir.path, eventFolderName)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } // --- START: MODIFICATION (FIXED ERROR) --- // This line is CORRECT. It uses data.toMap() to get a Map. final Map jsonData = data.toMap(); // --- END: MODIFICATION (FIXED ERROR) --- jsonData['submissionStatus'] = data.submissionStatus; jsonData['submissionMessage'] = data.submissionMessage; jsonData['serverConfigName'] = serverName; final imageFiles = data.toApiImageFiles(); for (var entry in imageFiles.entries) { final File? imageFile = entry.value; if (imageFile != null && imageFile.path.isNotEmpty) { try { if (p.dirname(imageFile.path) == eventDir.path) { jsonData[entry.key] = imageFile.path; } else { final String originalFileName = p.basename(imageFile.path); final File newFile = await imageFile.copy(p.join(eventDir.path, originalFileName)); jsonData[entry.key] = newFile.path; } } catch (e) { debugPrint("Error processing In-Situ image file ${imageFile.path}: $e"); jsonData[entry.key] = null; } } } final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(jsonData)); debugPrint("In-Situ log saved to: ${jsonFile.path}"); return eventDir.path; } catch (e) { debugPrint("Error saving In-Situ log to local storage: $e"); return null; } } Future>> getAllInSituLogs() async { final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); if (mmsv4Root == null || !await mmsv4Root.exists()) return []; final List> allLogs = []; final serverDirs = mmsv4Root.listSync().whereType(); for (var serverDir in serverDirs) { final baseDir = Directory(p.join(serverDir.path, 'marine', 'marine_in_situ_sampling')); if (!await baseDir.exists()) continue; try { final entities = baseDir.listSync(); for (var entity in entities) { if (entity is Directory) { final jsonFile = File(p.join(entity.path, 'data.json')); if (await jsonFile.exists()) { final content = await jsonFile.readAsString(); final data = jsonDecode(content) as Map; data['logDirectory'] = entity.path; allLogs.add(data); } } } } catch (e) { debugPrint("Error reading in-situ logs from ${baseDir.path}: $e"); } } return allLogs; } Future updateInSituLog(Map updatedLogData) async { final logDir = updatedLogData['logDirectory']; if (logDir == null) { debugPrint("Cannot update log: logDirectory key is missing."); return; } try { final jsonFile = File(p.join(logDir, 'data.json')); if (await jsonFile.exists()) { updatedLogData.remove('isResubmitting'); await jsonFile.writeAsString(jsonEncode(updatedLogData)); debugPrint("Log updated successfully at: ${jsonFile.path}"); } } catch (e) { debugPrint("Error updating in-situ log: $e"); } } Future> getRecentNearbySamples({ required double latitude, required double longitude, required double radiusKm, required int withinHours, }) async { final allLogs = await getAllInSituLogs(); final List recentNearbySamples = []; final cutoffDateTime = DateTime.now().subtract(Duration(hours: withinHours)); final double radiusInMeters = radiusKm * 1000; for (var log in allLogs) { try { final sampleData = InSituSamplingData.fromJson(log); if (sampleData.samplingDate == null || sampleData.samplingTime == null) { continue; } final sampleDateTime = DateTime.tryParse('${sampleData.samplingDate} ${sampleData.samplingTime}'); if (sampleDateTime == null || sampleDateTime.isBefore(cutoffDateTime)) { continue; } final sampleLat = double.tryParse(sampleData.currentLatitude ?? ''); final sampleLon = double.tryParse(sampleData.currentLongitude ?? ''); if (sampleLat == null || sampleLon == null) { continue; } final distanceInMeters = Geolocator.distanceBetween( latitude, longitude, sampleLat, sampleLon, ); if (distanceInMeters <= radiusInMeters) { recentNearbySamples.add(sampleData); } } catch (e) { debugPrint("Error processing in-situ log for nearby search: $e"); } } recentNearbySamples.sort((a, b) { final dtA = DateTime.tryParse('${a.samplingDate} ${a.samplingTime}'); final dtB = DateTime.tryParse('${b.samplingDate} ${b.samplingTime}'); if (dtA == null || dtB == null) return 0; return dtB.compareTo(dtA); }); return recentNearbySamples; } // --- ADDED: Part 4.5: Marine NPE Report Specific Methods --- Future _getNpeBaseDir({required String serverName}) async { final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName); if (mmsv4Dir == null) return null; final npeDir = Directory(p.join(mmsv4Dir.path, 'marine', 'marine_npe_report')); if (!await npeDir.exists()) { await npeDir.create(recursive: true); } return npeDir; } Future saveNpeReportData(MarineManualNpeReportData data, {required String serverName}) async { final baseDir = await _getNpeBaseDir(serverName: serverName); if (baseDir == null) { debugPrint("Could not get public storage directory for NPE. Check permissions."); return null; } try { final stationCode = data.selectedStation?['man_station_code'] ?? data.selectedStation?['tbl_station_code'] ?? 'CUSTOM_LOC'; final timestamp = "${data.eventDate}_${data.eventTime?.replaceAll(':', '-')}"; final eventFolderName = "${stationCode}_${timestamp}_NPE"; final eventDir = Directory(p.join(baseDir.path, eventFolderName)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } final Map jsonData = data.toDbJson(); jsonData['serverConfigName'] = serverName; final imageFiles = data.toApiImageFiles(); for (var entry in imageFiles.entries) { final File? imageFile = entry.value; if (imageFile != null && imageFile.path.isNotEmpty) { try { final String originalFileName = p.basename(imageFile.path); final File newFile = await imageFile.copy(p.join(eventDir.path, originalFileName)); jsonData['image${entry.key.split('_').last}Path'] = newFile.path; // Save as image1Path, etc. } catch (e) { debugPrint("Error processing NPE image file ${imageFile.path}: $e"); } } } final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(jsonData)); debugPrint("NPE Report log saved to: ${jsonFile.path}"); return eventDir.path; } catch (e) { debugPrint("Error saving NPE report to local storage: $e"); return null; } } Future>> getAllNpeLogs() async { final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); if (mmsv4Root == null || !await mmsv4Root.exists()) return []; final List> allLogs = []; final serverDirs = mmsv4Root.listSync().whereType(); for (var serverDir in serverDirs) { final baseDir = Directory(p.join(serverDir.path, 'marine', 'marine_npe_report')); if (!await baseDir.exists()) continue; try { final entities = baseDir.listSync(); for (var entity in entities) { if (entity is Directory) { final jsonFile = File(p.join(entity.path, 'data.json')); if (await jsonFile.exists()) { final content = await jsonFile.readAsString(); final data = jsonDecode(content) as Map; data['logDirectory'] = entity.path; allLogs.add(data); } } } } catch (e) { debugPrint("Error reading NPE logs from ${baseDir.path}: $e"); } } return allLogs; } Future updateNpeLog(Map updatedLogData) async { final logDir = updatedLogData['logDirectory']; if (logDir == null) return; try { final jsonFile = File(p.join(logDir, 'data.json')); if (await jsonFile.exists()) { updatedLogData.remove('isResubmitting'); await jsonFile.writeAsString(jsonEncode(updatedLogData)); debugPrint("NPE Log updated successfully at: ${jsonFile.path}"); } } catch (e) { debugPrint("Error updating NPE log: $e"); } } // ======================================================================= // --- START: Added Part 4.6: Marine Pre-Departure Methods --- // ======================================================================= Future _getPreDepartureBaseDir({required String serverName}) async { final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName); if (mmsv4Dir == null) return null; final logDir = Directory(p.join(mmsv4Dir.path, 'marine', 'marine_pre_departure')); if (!await logDir.exists()) { await logDir.create(recursive: true); } return logDir; } Future savePreDepartureData(MarineManualPreDepartureChecklistData data, {required String serverName}) async { final baseDir = await _getPreDepartureBaseDir(serverName: serverName); if (baseDir == null) return null; try { final timestamp = data.submissionDate ?? DateTime.now().toIso8601String(); final eventFolderName = "checklist_$timestamp"; final eventDir = Directory(p.join(baseDir.path, eventFolderName)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } // --- START: MODIFIED BLOCK --- // Use toDbJson() for a complete, consistent log final Map jsonData = data.toDbJson(); jsonData['serverConfigName'] = serverName; // All other fields are now included in toDbJson() // --- END: MODIFIED BLOCK --- final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(jsonData)); debugPrint("Pre-Departure log saved to: ${jsonFile.path}"); return eventDir.path; } catch (e) { debugPrint("Error saving Pre-Departure log to local storage: $e"); return null; } } Future>> getAllPreDepartureLogs() async { final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); if (mmsv4Root == null || !await mmsv4Root.exists()) return []; final List> allLogs = []; final serverDirs = mmsv4Root.listSync().whereType(); for (var serverDir in serverDirs) { final baseDir = Directory(p.join(serverDir.path, 'marine', 'marine_pre_departure')); if (!await baseDir.exists()) continue; try { final entities = baseDir.listSync(); for (var entity in entities) { if (entity is Directory) { final jsonFile = File(p.join(entity.path, 'data.json')); if (await jsonFile.exists()) { final data = jsonDecode(await jsonFile.readAsString()) as Map; data['logDirectory'] = entity.path; allLogs.add(data); } } } } catch (e) { debugPrint("Error reading Pre-Departure logs from ${baseDir.path}: $e"); } } return allLogs; } Future updatePreDepartureLog(Map updatedLogData) async { final logDir = updatedLogData['logDirectory']; if (logDir == null) return; try { final jsonFile = File(p.join(logDir, 'data.json')); if (await jsonFile.exists()) { updatedLogData.remove('isResubmitting'); await jsonFile.writeAsString(jsonEncode(updatedLogData)); } } catch (e) { debugPrint("Error updating Pre-Departure log: $e"); } } // ======================================================================= // --- START: Added Part 4.7: Marine Sonde Calibration Methods --- // ======================================================================= Future _getSondeCalibrationBaseDir({required String serverName}) async { final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName); if (mmsv4Dir == null) return null; final logDir = Directory(p.join(mmsv4Dir.path, 'marine', 'marine_sonde_calibration')); if (!await logDir.exists()) { await logDir.create(recursive: true); } return logDir; } Future saveSondeCalibrationData(MarineManualSondeCalibrationData data, {required String serverName}) async { final baseDir = await _getSondeCalibrationBaseDir(serverName: serverName); if (baseDir == null) return null; try { final timestamp = data.startDateTime?.replaceAll(':', '-').replaceAll(' ', '_') ?? DateTime.now().toIso8601String(); final eventFolderName = "calibration_${data.sondeSerialNumber}_$timestamp"; final eventDir = Directory(p.join(baseDir.path, eventFolderName)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } // --- START: MODIFIED BLOCK --- // Use toDbJson() for a complete, consistent log final Map jsonData = data.toDbJson(); jsonData['serverConfigName'] = serverName; // All other fields are now included in toDbJson() // --- END: MODIFIED BLOCK --- final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(jsonData)); debugPrint("Sonde Calibration log saved to: ${jsonFile.path}"); return eventDir.path; } catch (e) { debugPrint("Error saving Sonde Calibration log to local storage: $e"); return null; } } Future>> getAllSondeCalibrationLogs() async { final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); if (mmsv4Root == null || !await mmsv4Root.exists()) return []; final List> allLogs = []; final serverDirs = mmsv4Root.listSync().whereType(); for (var serverDir in serverDirs) { final baseDir = Directory(p.join(serverDir.path, 'marine', 'marine_sonde_calibration')); if (!await baseDir.exists()) continue; try { final entities = baseDir.listSync(); for (var entity in entities) { if (entity is Directory) { final jsonFile = File(p.join(entity.path, 'data.json')); if (await jsonFile.exists()) { final data = jsonDecode(await jsonFile.readAsString()) as Map; data['logDirectory'] = entity.path; allLogs.add(data); } } } } catch (e) { debugPrint("Error reading Sonde Calibration logs from ${baseDir.path}: $e"); } } return allLogs; } Future updateSondeCalibrationLog(Map updatedLogData) async { final logDir = updatedLogData['logDirectory']; if (logDir == null) return; try { final jsonFile = File(p.join(logDir, 'data.json')); if (await jsonFile.exists()) { updatedLogData.remove('isResubmitting'); await jsonFile.writeAsString(jsonEncode(updatedLogData)); } } catch (e) { debugPrint("Error updating Sonde Calibration log: $e"); } } // ======================================================================= // --- START: Added Part 4.8: Marine Equipment Maintenance Methods --- // ======================================================================= Future _getEquipmentMaintenanceBaseDir({required String serverName}) async { final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName); if (mmsv4Dir == null) return null; final logDir = Directory(p.join(mmsv4Dir.path, 'marine', 'marine_equipment_maintenance')); if (!await logDir.exists()) { await logDir.create(recursive: true); } return logDir; } Future saveEquipmentMaintenanceData(MarineManualEquipmentMaintenanceData data, {required String serverName}) async { final baseDir = await _getEquipmentMaintenanceBaseDir(serverName: serverName); if (baseDir == null) return null; try { final timestamp = data.maintenanceDate ?? DateTime.now().toIso8601String(); final eventFolderName = "maintenance_$timestamp"; final eventDir = Directory(p.join(baseDir.path, eventFolderName)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } // --- START: MODIFIED BLOCK --- // Use toDbJson() for a complete, consistent log final Map jsonData = data.toDbJson(); jsonData['serverConfigName'] = serverName; // All other fields are now included in toDbJson() // --- END: MODIFIED BLOCK --- final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(jsonData)); debugPrint("Equipment Maintenance log saved to: ${jsonFile.path}"); return eventDir.path; } catch (e) { debugPrint("Error saving Equipment Maintenance log to local storage: $e"); return null; } } Future>> getAllEquipmentMaintenanceLogs() async { final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); if (mmsv4Root == null || !await mmsv4Root.exists()) return []; final List> allLogs = []; final serverDirs = mmsv4Root.listSync().whereType(); for (var serverDir in serverDirs) { final baseDir = Directory(p.join(serverDir.path, 'marine', 'marine_equipment_maintenance')); if (!await baseDir.exists()) continue; try { final entities = baseDir.listSync(); for (var entity in entities) { if (entity is Directory) { final jsonFile = File(p.join(entity.path, 'data.json')); if (await jsonFile.exists()) { final data = jsonDecode(await jsonFile.readAsString()) as Map; data['logDirectory'] = entity.path; allLogs.add(data); } } } } catch (e) { debugPrint("Error reading Equipment Maintenance logs from ${baseDir.path}: $e"); } } return allLogs; } Future updateEquipmentMaintenanceLog(Map updatedLogData) async { final logDir = updatedLogData['logDirectory']; if (logDir == null) return; try { final jsonFile = File(p.join(logDir, 'data.json')); if (await jsonFile.exists()) { updatedLogData.remove('isResubmitting'); await jsonFile.writeAsString(jsonEncode(updatedLogData)); } } catch (e) { debugPrint("Error updating Equipment Maintenance log: $e"); } } // ======================================================================= // Part 5: River In-Situ Specific Methods (LOGGING RESTORED) // ======================================================================= Future getRiverInSituBaseDir(String? samplingType, {required String serverName}) async { final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName); if (mmsv4Dir == null) return null; String subfolderName; if (samplingType == 'Schedule' || samplingType == 'Triennial') { subfolderName = samplingType!; } else { subfolderName = 'Others'; } final inSituDir = Directory(p.join(mmsv4Dir.path, 'river', 'river_in_situ_sampling', subfolderName)); if (!await inSituDir.exists()) { await inSituDir.create(recursive: true); } return inSituDir; } Future saveRiverInSituSamplingData(RiverInSituSamplingData data, {required String serverName}) async { final baseDir = await getRiverInSituBaseDir(data.samplingType, serverName: serverName); if (baseDir == null) { debugPrint("Could not get public storage directory for River In-Situ. Check permissions."); return null; } try { final stationCode = data.selectedStation?['sampling_station_code'] ?? 'UNKNOWN_STATION'; final timestamp = "${data.samplingDate}_${data.samplingTime?.replaceAll(':', '-')}"; final eventFolderName = "${stationCode}_$timestamp"; final eventDir = Directory(p.join(baseDir.path, eventFolderName)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } final Map jsonData = data.toMap(); jsonData['serverConfigName'] = serverName; final imageFiles = data.toApiImageFiles(); for (var entry in imageFiles.entries) { final File? imageFile = entry.value; if (imageFile != null) { final String originalFileName = p.basename(imageFile.path); if (p.dirname(imageFile.path) == eventDir.path) { jsonData[entry.key] = imageFile.path; } else { final File newFile = await imageFile.copy(p.join(eventDir.path, originalFileName)); jsonData[entry.key] = newFile.path; } } } final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(jsonData)); debugPrint("River In-Situ log saved to: ${jsonFile.path}"); return eventDir.path; } catch (e) { debugPrint("Error saving River In-Situ log to local storage: $e"); return null; } } Future>> getAllRiverInSituLogs() async { final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); if (mmsv4Root == null || !await mmsv4Root.exists()) return []; final List> allLogs = []; final serverDirs = mmsv4Root.listSync().whereType(); for (var serverDir in serverDirs) { final topLevelDir = Directory(p.join(serverDir.path, 'river', 'river_in_situ_sampling')); if (!await topLevelDir.exists()) continue; try { final typeSubfolders = topLevelDir.listSync(); for (var typeSubfolder in typeSubfolders) { if (typeSubfolder is Directory) { final eventFolders = typeSubfolder.listSync(); for (var eventFolder in eventFolders) { if (eventFolder is Directory) { final jsonFile = File(p.join(eventFolder.path, 'data.json')); if (await jsonFile.exists()) { final content = await jsonFile.readAsString(); final data = jsonDecode(content) as Map; data['logDirectory'] = eventFolder.path; allLogs.add(data); } } } } } } catch (e) { debugPrint("Error getting all river in-situ logs from ${topLevelDir.path}: $e"); } } return allLogs; } Future updateRiverInSituLog(Map updatedLogData) async { final logDir = updatedLogData['logDirectory']; if (logDir == null) { debugPrint("Cannot update log: logDirectory key is missing."); return; } try { final jsonFile = File(p.join(logDir, 'data.json')); if (await jsonFile.exists()) { updatedLogData.remove('isResubmitting'); await jsonFile.writeAsString(jsonEncode(updatedLogData)); debugPrint("Log updated successfully at: ${jsonFile.path}"); } } catch (e) { debugPrint("Error updating river in-situ log: $e"); } } // ======================================================================= // Part 6: River Triennial Specific Methods // ======================================================================= Future _getRiverTriennialBaseDir({required String serverName}) async { final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName); if (mmsv4Dir == null) return null; final triennialDir = Directory(p.join(mmsv4Dir.path, 'river', 'river_triennial_sampling')); if (!await triennialDir.exists()) { await triennialDir.create(recursive: true); } return triennialDir; } Future saveRiverManualTriennialSamplingData(RiverManualTriennialSamplingData data, {required String serverName}) async { final baseDir = await _getRiverTriennialBaseDir(serverName: serverName); if (baseDir == null) { debugPrint("Could not get public storage directory for River Triennial. Check permissions."); return null; } try { final stationCode = data.selectedStation?['sampling_station_code'] ?? 'UNKNOWN_STATION'; final timestamp = "${data.samplingDate}_${data.samplingTime?.replaceAll(':', '-')}"; final eventFolderName = "${stationCode}_$timestamp"; final eventDir = Directory(p.join(baseDir.path, eventFolderName)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } final Map jsonData = data.toMap(); jsonData['serverConfigName'] = serverName; final imageFiles = data.toApiImageFiles(); for (var entry in imageFiles.entries) { final File? imageFile = entry.value; if (imageFile != null) { final String originalFileName = p.basename(imageFile.path); if (p.dirname(imageFile.path) == eventDir.path) { jsonData[entry.key] = imageFile.path; } else { final File newFile = await imageFile.copy(p.join(eventDir.path, originalFileName)); jsonData[entry.key] = newFile.path; } } } final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(jsonData)); debugPrint("River Triennial log saved to: ${jsonFile.path}"); return eventDir.path; } catch (e) { debugPrint("Error saving River Triennial log to local storage: $e"); return null; } } Future>> getAllRiverManualTriennialLogs() async { final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); if (mmsv4Root == null || !await mmsv4Root.exists()) return []; final List> allLogs = []; final serverDirs = mmsv4Root.listSync().whereType(); for (var serverDir in serverDirs) { final baseDir = Directory(p.join(serverDir.path, 'river', 'river_triennial_sampling')); if (!await baseDir.exists()) continue; try { final entities = baseDir.listSync(); for (var entity in entities) { if (entity is Directory) { final jsonFile = File(p.join(entity.path, 'data.json')); if (await jsonFile.exists()) { final content = await jsonFile.readAsString(); final data = jsonDecode(content) as Map; data['logDirectory'] = entity.path; allLogs.add(data); } } } } catch (e) { debugPrint("Error reading triennial logs from ${baseDir.path}: $e"); } } return allLogs; } Future updateRiverManualTriennialLog(Map updatedLogData) async { final logDir = updatedLogData['logDirectory']; if (logDir == null) { debugPrint("Cannot update log: logDirectory key is missing."); return; } try { final jsonFile = File(p.join(logDir, 'data.json')); if (await jsonFile.exists()) { updatedLogData.remove('isResubmitting'); await jsonFile.writeAsString(jsonEncode(updatedLogData)); debugPrint("Log updated successfully at: ${jsonFile.path}"); } } catch (e) { debugPrint("Error updating river triennial log: $e"); } } // ======================================================================= // --- ADDED: Part 6.5: Marine Investigative Specific Methods --- // ======================================================================= Future _getInvestigativeBaseDir({required String serverName}) async { final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName); if (mmsv4Dir == null) return null; // Use a new subModule path for investigative logs final inSituDir = Directory(p.join(mmsv4Dir.path, 'marine', 'marine_investigative_sampling')); if (!await inSituDir.exists()) { await inSituDir.create(recursive: true); } return inSituDir; } /// Saves Marine Investigative sampling data to the local log Future saveInvestigativeSamplingData(MarineInvesManualSamplingData data, {required String serverName}) async { final baseDir = await _getInvestigativeBaseDir(serverName: serverName); if (baseDir == null) { debugPrint("Could not get public storage directory for Investigative. Check permissions."); return null; } try { // --- Generate folder name based on station type --- String stationCode = 'NA'; if (data.stationTypeSelection == 'Existing Manual Station') { stationCode = data.selectedStation?['man_station_code'] ?? 'MANUAL_NA'; } else if (data.stationTypeSelection == 'Existing Tarball Station') { stationCode = data.selectedTarballStation?['tbl_station_code'] ?? 'TARBALL_NA'; } else if (data.stationTypeSelection == 'New Location') { stationCode = data.newStationCode ?? 'NEW_NA'; } final timestamp = "${data.samplingDate}_${data.samplingTime?.replaceAll(':', '-')}"; final eventFolderName = "${stationCode}_$timestamp"; final eventDir = Directory(p.join(baseDir.path, eventFolderName)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } final Map jsonData = data.toDbJson(); // jsonData['submissionStatus'] = data.submissionStatus; jsonData['submissionMessage'] = data.submissionMessage; jsonData['reportId'] = data.reportId; jsonData['serverConfigName'] = serverName; final imageFiles = data.toApiImageFiles(); // for (var entry in imageFiles.entries) { final File? imageFile = entry.value; if (imageFile != null && imageFile.path.isNotEmpty) { try { // Check if file is already in the correct directory (e.g., from a retry) if (p.dirname(imageFile.path) == eventDir.path) { jsonData[entry.key] = imageFile.path; } else { // Copy file from temp cache to persistent log directory final String originalFileName = p.basename(imageFile.path); final File newFile = await imageFile.copy(p.join(eventDir.path, originalFileName)); jsonData[entry.key] = newFile.path; } } catch (e) { debugPrint("Error processing Investigative image file ${imageFile.path}: $e"); jsonData[entry.key] = null; } } } final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(jsonData)); debugPrint("Investigative log saved to: ${jsonFile.path}"); return eventDir.path; // Return the path to the saved log directory } catch (e) { debugPrint("Error saving Investigative log to local storage: $e"); return null; } } /// Fetches all saved Marine Investigative logs Future>> getAllInvestigativeLogs() async { final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); if (mmsv4Root == null || !await mmsv4Root.exists()) return []; final List> allLogs = []; final serverDirs = mmsv4Root.listSync().whereType(); for (var serverDir in serverDirs) { final baseDir = Directory(p.join(serverDir.path, 'marine', 'marine_investigative_sampling')); if (!await baseDir.exists()) continue; try { final entities = baseDir.listSync(); for (var entity in entities) { if (entity is Directory) { final jsonFile = File(p.join(entity.path, 'data.json')); if (await jsonFile.exists()) { final content = await jsonFile.readAsString(); final data = jsonDecode(content) as Map; data['logDirectory'] = entity.path; allLogs.add(data); } } } } catch (e) { debugPrint("Error reading investigative logs from ${baseDir.path}: $e"); } } return allLogs; } /// Updates an existing Marine Investigative log file (e.g., after a retry) Future updateInvestigativeLog(Map updatedLogData) async { final logDir = updatedLogData['logDirectory']; if (logDir == null) { debugPrint("Cannot update investigative log: logDirectory key is missing."); return; } try { final jsonFile = File(p.join(logDir, 'data.json')); if (await jsonFile.exists()) { updatedLogData.remove('isResubmitting'); // Clean up temporary flags await jsonFile.writeAsString(jsonEncode(updatedLogData)); debugPrint("Investigative log updated successfully at: ${jsonFile.path}"); } } catch (e) { debugPrint("Error updating investigative log: $e"); } } // ======================================================================= // --- ADDED: Part 6.6: River Investigative Specific Methods --- // ======================================================================= /// Gets the base directory for River Investigative logs. Future getRiverInvestigativeBaseDir({required String serverName}) async { final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName); if (mmsv4Dir == null) return null; final invesDir = Directory(p.join(mmsv4Dir.path, 'river', 'river_investigative_sampling')); if (!await invesDir.exists()) { await invesDir.create(recursive: true); } return invesDir; } /// Saves River Investigative sampling data to the local log. Future saveRiverInvestigativeSamplingData(RiverInvesManualSamplingData data, {required String serverName}) async { final baseDir = await getRiverInvestigativeBaseDir(serverName: serverName); if (baseDir == null) { debugPrint("Could not get public storage directory for River Investigative. Check permissions."); return null; } try { final stationCode = data.getDeterminedStationCode() ?? 'UNKNOWN'; final timestamp = "${data.samplingDate}_${data.samplingTime?.replaceAll(':', '-')}"; final eventFolderName = "${stationCode}_$timestamp"; final eventDir = Directory(p.join(baseDir.path, eventFolderName)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } // Use the .toMap() method from the data model, which is designed for local logging final Map jsonData = data.toMap(); jsonData['serverConfigName'] = serverName; // Status, message, and reportId are already included by .toMap() final imageFiles = data.toApiImageFiles(); for (var entry in imageFiles.entries) { final File? imageFile = entry.value; if (imageFile != null && imageFile.path.isNotEmpty) { try { if (p.dirname(imageFile.path) == eventDir.path) { jsonData[entry.key] = imageFile.path; // Already in log dir } else { final String originalFileName = p.basename(imageFile.path); final File newFile = await imageFile.copy(p.join(eventDir.path, originalFileName)); jsonData[entry.key] = newFile.path; // Store the new persistent path } } catch (e) { debugPrint("Error processing River Investigative image file ${imageFile.path}: $e"); jsonData[entry.key] = null; // Store null if copy failed } } else { // Ensure keys for null images are also present if needed, though .toMap() handles this jsonData[entry.key] = null; } } final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(jsonData)); debugPrint("River Investigative log saved to: ${jsonFile.path}"); return eventDir.path; } catch (e) { debugPrint("Error saving River Investigative log to local storage: $e"); return null; } } /// Fetches all saved River Investigative logs. Future>> getAllRiverInvestigativeLogs() async { final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); if (mmsv4Root == null || !await mmsv4Root.exists()) return []; final List> allLogs = []; final serverDirs = mmsv4Root.listSync().whereType(); for (var serverDir in serverDirs) { final baseDir = Directory(p.join(serverDir.path, 'river', 'river_investigative_sampling')); if (!await baseDir.exists()) continue; try { final entities = baseDir.listSync(); for (var entity in entities) { if (entity is Directory) { final jsonFile = File(p.join(entity.path, 'data.json')); if (await jsonFile.exists()) { final content = await jsonFile.readAsString(); final data = jsonDecode(content) as Map; data['logDirectory'] = entity.path; allLogs.add(data); } } } } catch (e) { debugPrint("Error reading river investigative logs from ${baseDir.path}: $e"); } } return allLogs; } /// Updates an existing River Investigative log file (e.g., after a retry). Future updateRiverInvestigativeLog(Map updatedLogData) async { final logDir = updatedLogData['logDirectory']; if (logDir == null) { debugPrint("Cannot update river investigative log: logDirectory key is missing."); return; } try { final jsonFile = File(p.join(logDir, 'data.json')); if (await jsonFile.exists()) { updatedLogData.remove('isResubmitting'); // Clean up temporary flags await jsonFile.writeAsString(jsonEncode(updatedLogData)); debugPrint("River Investigative log updated successfully at: ${jsonFile.path}"); } } catch (e) { debugPrint("Error updating river investigative log: $e"); } } // ======================================================================= // --- ADDED: Part 7: Info Centre Document Management --- // ======================================================================= final Dio _dio = Dio(); Future _getInfoCentreDocumentsDirectory() async { final mmsv4Dir = await _getPublicMMSV4Directory(serverName: ''); if (mmsv4Dir == null) return null; final docDir = Directory(p.join(mmsv4Dir.path, 'info_centre_documents')); if (!await docDir.exists()) { await docDir.create(recursive: true); } return docDir; } Future getLocalDocumentPath(String docUrl) async { final docDir = await _getInfoCentreDocumentsDirectory(); if (docDir == null) return null; final fileName = p.basename(docUrl); return p.join(docDir.path, fileName); } Future isDocumentDownloaded(String docUrl) async { final filePath = await getLocalDocumentPath(docUrl); if (filePath == null) return false; return await File(filePath).exists(); } Future downloadDocument({ required String docUrl, required Function(double) onReceiveProgress, }) async { final filePath = await getLocalDocumentPath(docUrl); if (filePath == null) { throw Exception("Could not get local storage path. Check permissions."); } try { await _dio.download( docUrl, filePath, onReceiveProgress: (received, total) { if (total != -1) { onReceiveProgress(received / total); } }, ); } catch (e) { final file = File(filePath); if (await file.exists()) { await file.delete(); } throw Exception("Download failed: $e"); } } }