environment_monitoring_app/lib/services/local_storage_service.dart

1394 lines
53 KiB
Dart

// 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<bool> _requestPermissions() async {
var status = await Permission.manageExternalStorage.request();
return status.isGranted;
}
Future<Directory?> _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<Directory?> 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<Directory?> _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<String?> saveAirSamplingRecord(Map<String, dynamic> 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<String?> 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<String, dynamic> 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<String, dynamic>.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<String, dynamic> finalData = Map.from(serializableData);
void cleanMap(Map<String, dynamic> map) {
map.removeWhere((key, value) => value is File);
map.forEach((key, value) {
if (value is Map) cleanMap(value as Map<String, dynamic>);
});
}
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<List<Map<String, dynamic>>> getAllAirSamplingLogs() async {
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
final List<Map<String, dynamic>> allLogs = [];
final serverDirs = mmsv4Root.listSync().whereType<Directory>();
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<String, dynamic>;
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<Directory?> _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<String?> 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<String, dynamic> 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<List<Map<String, dynamic>>> getAllTarballLogs() async {
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
final List<Map<String, dynamic>> allLogs = [];
final serverDirs = mmsv4Root.listSync().whereType<Directory>();
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<String, dynamic>;
data['logDirectory'] = entity.path;
allLogs.add(data);
}
}
}
} catch (e) {
debugPrint("Error reading tarball logs from ${baseDir.path}: $e");
}
}
return allLogs;
}
Future<void> updateTarballLog(Map<String, dynamic> 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<Directory?> 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<String?> 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);
}
final Map<String, dynamic> jsonData = data.toDbJson();
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<List<Map<String, dynamic>>> getAllInSituLogs() async {
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
final List<Map<String, dynamic>> allLogs = [];
final serverDirs = mmsv4Root.listSync().whereType<Directory>();
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<String, dynamic>;
data['logDirectory'] = entity.path;
allLogs.add(data);
}
}
}
} catch (e) {
debugPrint("Error reading in-situ logs from ${baseDir.path}: $e");
}
}
return allLogs;
}
Future<void> updateInSituLog(Map<String, dynamic> 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<List<InSituSamplingData>> getRecentNearbySamples({
required double latitude,
required double longitude,
required double radiusKm,
required int withinHours,
}) async {
final allLogs = await getAllInSituLogs();
final List<InSituSamplingData> 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<Directory?> _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<String?> 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<String, dynamic> 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<List<Map<String, dynamic>>> getAllNpeLogs() async {
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
final List<Map<String, dynamic>> allLogs = [];
final serverDirs = mmsv4Root.listSync().whereType<Directory>();
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<String, dynamic>;
data['logDirectory'] = entity.path;
allLogs.add(data);
}
}
}
} catch (e) {
debugPrint("Error reading NPE logs from ${baseDir.path}: $e");
}
}
return allLogs;
}
Future<void> updateNpeLog(Map<String, dynamic> 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<Directory?> _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<String?> 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<String, dynamic> 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<List<Map<String, dynamic>>> getAllPreDepartureLogs() async {
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
final List<Map<String, dynamic>> allLogs = [];
final serverDirs = mmsv4Root.listSync().whereType<Directory>();
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<String, dynamic>;
data['logDirectory'] = entity.path;
allLogs.add(data);
}
}
}
} catch (e) {
debugPrint("Error reading Pre-Departure logs from ${baseDir.path}: $e");
}
}
return allLogs;
}
Future<void> updatePreDepartureLog(Map<String, dynamic> 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<Directory?> _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<String?> 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<String, dynamic> 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<List<Map<String, dynamic>>> getAllSondeCalibrationLogs() async {
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
final List<Map<String, dynamic>> allLogs = [];
final serverDirs = mmsv4Root.listSync().whereType<Directory>();
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<String, dynamic>;
data['logDirectory'] = entity.path;
allLogs.add(data);
}
}
}
} catch (e) {
debugPrint("Error reading Sonde Calibration logs from ${baseDir.path}: $e");
}
}
return allLogs;
}
Future<void> updateSondeCalibrationLog(Map<String, dynamic> 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<Directory?> _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<String?> 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<String, dynamic> 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<List<Map<String, dynamic>>> getAllEquipmentMaintenanceLogs() async {
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
final List<Map<String, dynamic>> allLogs = [];
final serverDirs = mmsv4Root.listSync().whereType<Directory>();
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<String, dynamic>;
data['logDirectory'] = entity.path;
allLogs.add(data);
}
}
}
} catch (e) {
debugPrint("Error reading Equipment Maintenance logs from ${baseDir.path}: $e");
}
}
return allLogs;
}
Future<void> updateEquipmentMaintenanceLog(Map<String, dynamic> 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<Directory?> 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<String?> 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<String, dynamic> 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<List<Map<String, dynamic>>> getAllRiverInSituLogs() async {
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
final List<Map<String, dynamic>> allLogs = [];
final serverDirs = mmsv4Root.listSync().whereType<Directory>();
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<String, dynamic>;
data['logDirectory'] = eventFolder.path;
allLogs.add(data);
}
}
}
}
}
} catch (e) {
debugPrint("Error getting all river in-situ logs from ${topLevelDir.path}: $e");
}
}
return allLogs;
}
Future<void> updateRiverInSituLog(Map<String, dynamic> 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<Directory?> _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<String?> 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<String, dynamic> 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<List<Map<String, dynamic>>> getAllRiverManualTriennialLogs() async {
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
final List<Map<String, dynamic>> allLogs = [];
final serverDirs = mmsv4Root.listSync().whereType<Directory>();
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<String, dynamic>;
data['logDirectory'] = entity.path;
allLogs.add(data);
}
}
}
} catch (e) {
debugPrint("Error reading triennial logs from ${baseDir.path}: $e");
}
}
return allLogs;
}
Future<void> updateRiverManualTriennialLog(Map<String, dynamic> 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<Directory?> _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<String?> 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<String, dynamic> 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<List<Map<String, dynamic>>> getAllInvestigativeLogs() async {
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
final List<Map<String, dynamic>> allLogs = [];
final serverDirs = mmsv4Root.listSync().whereType<Directory>();
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<String, dynamic>;
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<void> updateInvestigativeLog(Map<String, dynamic> 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<Directory?> 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<String?> 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<String, dynamic> 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<List<Map<String, dynamic>>> getAllRiverInvestigativeLogs() async {
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
final List<Map<String, dynamic>> allLogs = [];
final serverDirs = mmsv4Root.listSync().whereType<Directory>();
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<String, dynamic>;
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<void> updateRiverInvestigativeLog(Map<String, dynamic> 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<Directory?> _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<String?> 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<bool> isDocumentDownloaded(String docUrl) async {
final filePath = await getLocalDocumentPath(docUrl);
if (filePath == null) return false;
return await File(filePath).exists();
}
Future<void> 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");
}
}
}