environment_monitoring_app/lib/services/local_storage_service.dart

462 lines
16 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 '../models/air_installation_data.dart';
import '../models/tarball_data.dart';
import '../models/in_situ_sampling_data.dart';
import '../models/river_in_situ_sampling_data.dart';
/// A comprehensive service for handling all local data storage for offline submissions.
class LocalStorageService {
// =======================================================================
// Part 1: Public Storage Setup
// =======================================================================
Future<bool> _requestPermissions() async {
var status = await Permission.manageExternalStorage.request();
return status.isGranted;
}
Future<Directory?> _getPublicMMSV4Directory() 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'));
if (!await mmsv4Dir.exists()) {
await mmsv4Dir.create(recursive: true);
}
return mmsv4Dir;
}
}
debugPrint("LocalStorageService: Manage External Storage permission was not granted.");
return null;
}
// =======================================================================
// --- UPDATED: Part 2: Air Manual Sampling Methods ---
// =======================================================================
Future<Directory?> _getAirManualBaseDir() async {
final mmsv4Dir = await _getPublicMMSV4Directory();
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;
}
/// Saves or updates an air sampling record to a local JSON file within a folder named by its refID.
/// CORRECTED: This now accepts a generic Map, which is what the service layer provides.
Future<String?> saveAirSamplingRecord(Map<String, dynamic> data, String refID) async {
final baseDir = await _getAirManualBaseDir();
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);
}
// Helper function to copy a file and return its new path
Future<String?> copyImageToLocal(File? imageFile) async {
if (imageFile == null) return null;
try {
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;
}
}
// This logic is now more robust; it checks for File objects and copies them if they exist.
final AirInstallationData tempInstallationData = AirInstallationData.fromJson(data);
data['imageFrontPath'] = await copyImageToLocal(tempInstallationData.imageFront);
data['imageBackPath'] = await copyImageToLocal(tempInstallationData.imageBack);
data['imageLeftPath'] = await copyImageToLocal(tempInstallationData.imageLeft);
data['imageRightPath'] = await copyImageToLocal(tempInstallationData.imageRight);
data['optionalImage1Path'] = await copyImageToLocal(tempInstallationData.optionalImage1);
data['optionalImage2Path'] = await copyImageToLocal(tempInstallationData.optionalImage2);
data['optionalImage3Path'] = await copyImageToLocal(tempInstallationData.optionalImage3);
data['optionalImage4Path'] = await copyImageToLocal(tempInstallationData.optionalImage4);
final jsonFile = File(p.join(eventDir.path, 'data.json'));
await jsonFile.writeAsString(jsonEncode(data));
debugPrint("Air sampling log and images saved to: ${eventDir.path}");
return eventDir.path;
} catch (e) {
debugPrint("Error saving air sampling log to local storage: $e");
return null;
}
}
Future<List<Map<String, dynamic>>> getAllAirSamplingLogs() async {
final baseDir = await _getAirManualBaseDir();
if (baseDir == null || !await baseDir.exists()) return [];
try {
final List<Map<String, dynamic>> logs = [];
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;
logs.add(data);
}
}
}
return logs;
} catch (e) {
debugPrint("Error getting all air sampling logs: $e");
return [];
}
}
// =======================================================================
// Part 3: Tarball Specific Methods
// =======================================================================
Future<Directory?> _getTarballBaseDir() async {
final mmsv4Dir = await _getPublicMMSV4Directory();
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) async {
final baseDir = await _getTarballBaseDir();
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['selectedStation'] = data.selectedStation;
final imageFiles = data.toImageFiles();
for (var entry in imageFiles.entries) {
final File? imageFile = entry.value;
if (imageFile != null) {
final String originalFileName = p.basename(imageFile.path);
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("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 baseDir = await _getTarballBaseDir();
if (baseDir == null || !await baseDir.exists()) return [];
try {
final List<Map<String, dynamic>> logs = [];
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;
logs.add(data);
}
}
}
return logs;
} catch (e) {
debugPrint("Error getting all tarball logs: $e");
return [];
}
}
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
// =======================================================================
Future<Directory?> _getInSituBaseDir() async {
final mmsv4Dir = await _getPublicMMSV4Directory();
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) async {
final baseDir = await _getInSituBaseDir();
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.toApiFormData(), 'submissionStatus': data.submissionStatus, 'submissionMessage': data.submissionMessage, 'reportId': data.reportId };
jsonData['selectedStation'] = data.selectedStation;
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);
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("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 baseDir = await _getInSituBaseDir();
if (baseDir == null || !await baseDir.exists()) return [];
try {
final List<Map<String, dynamic>> logs = [];
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;
logs.add(data);
}
}
}
return logs;
} catch (e) {
debugPrint("Error getting all in-situ logs: $e");
return [];
}
}
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");
}
}
// =======================================================================
// UPDATED: Part 5: River In-Situ Specific Methods
// =======================================================================
Future<Directory?> _getRiverInSituBaseDir(String? samplingType) async {
final mmsv4Dir = await _getPublicMMSV4Directory();
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) async {
final baseDir = await _getRiverInSituBaseDir(data.samplingType);
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.toApiFormData(), 'submissionStatus': data.submissionStatus, 'submissionMessage': data.submissionMessage, 'reportId': data.reportId };
jsonData['selectedStation'] = data.selectedStation;
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);
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 mmsv4Dir = await _getPublicMMSV4Directory();
if (mmsv4Dir == null) return [];
final topLevelDir = Directory(p.join(mmsv4Dir.path, 'river', 'river_in_situ_sampling'));
if (!await topLevelDir.exists()) return [];
try {
final List<Map<String, dynamic>> logs = [];
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;
logs.add(data);
}
}
}
}
}
return logs;
} catch (e) {
debugPrint("Error getting all river in-situ logs: $e");
return [];
}
}
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");
}
}
}