From 18e853ac83a116b2b70f6cfa1799d059490073d5 Mon Sep 17 00:00:00 2001 From: ALim Aidrus Date: Wed, 19 Nov 2025 14:21:10 +0800 Subject: [PATCH] change river ftp folder name and image naming to follow mms 1.00 to edc --- lib/home_page.dart | 2 +- lib/screens/settings.dart | 2 +- .../river_in_situ_sampling_service.dart | 90 ++++++++++++++---- .../river_investigative_sampling_service.dart | 86 ++++++++++++++---- ...ver_manual_triennial_sampling_service.dart | 91 +++++++++++++++---- lib/services/zipping_service.dart | 70 ++++++++++---- 6 files changed, 264 insertions(+), 77 deletions(-) diff --git a/lib/home_page.dart b/lib/home_page.dart index f9cf145..3a625ce 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -29,7 +29,7 @@ class _HomePageState extends State { }); }, ), - title: const Text("MMS Version 3.8.01"), + title: const Text("MMS Version 3.12.01"), actions: [ IconButton( icon: const Icon(Icons.person), diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index ee52449..fc8ed83 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -221,7 +221,7 @@ class _SettingsScreenState extends State { ListTile( leading: const Icon(Icons.info_outline), title: const Text('App Version'), - subtitle: const Text('MMS Version 3.8.01'), + subtitle: const Text('MMS Version 3.12.01'), dense: true, ), ListTile( diff --git a/lib/services/river_in_situ_sampling_service.dart b/lib/services/river_in_situ_sampling_service.dart index fe198bb..530d8bc 100644 --- a/lib/services/river_in_situ_sampling_service.dart +++ b/lib/services/river_in_situ_sampling_service.dart @@ -355,19 +355,38 @@ class RiverInSituSamplingService { } if (finalImageFiles.isNotEmpty) { - final imageZip = await _zippingService.createImageZip( - imageFiles: finalImageFiles.values.toList(), + // Re-construct the map for retry to attempt renaming even in fallback + final Map retryImages = {}; + final String dateStr = (data.samplingDate ?? '').replaceAll('-', ''); + final String timeStr = (data.samplingTime ?? '').replaceAll(':', ''); + final String timestampId = "$dateStr$timeStr"; + + void addRetryMap(File? file, String prefix) { + if(file != null) retryImages['${prefix}_$timestampId.jpg'] = file; + } + addRetryMap(data.backgroundStationImage, 'background'); + addRetryMap(data.upstreamRiverImage, 'upstream'); + addRetryMap(data.downstreamRiverImage, 'downstream'); + addRetryMap(data.sampleTurbidityImage, 'sample_turbidity'); + addRetryMap(data.optionalImage1, 'optional_1'); + addRetryMap(data.optionalImage2, 'optional_2'); + addRetryMap(data.optionalImage3, 'optional_3'); + addRetryMap(data.optionalImage4, 'optional_4'); + + final retryImageZip = await _zippingService.createRenamedImageZip( + imageFiles: retryImages, baseFileName: baseFileNameForQueue, destinationDir: null, ); - if (imageZip != null) { + + if (retryImageZip != null) { // Queue for each config separately for (final config in ftpConfigs) { final configId = config['ftp_config_id']; if (configId != null) { await _retryService.addFtpToQueue( - localFilePath: imageZip.path, - remotePath: '/${p.basename(imageZip.path)}', + localFilePath: retryImageZip.path, + remotePath: '/${p.basename(retryImageZip.path)}', ftpConfigId: configId // Provide the specific config ID ); } @@ -502,21 +521,26 @@ class RiverInSituSamplingService { /// Generates data and image ZIP files and uploads them using SubmissionFtpService. Future> _generateAndUploadFtpFiles(RiverInSituSamplingData data, Map imageFiles, String serverName, String moduleName) async { + + // 1. GENERATE TIMESTAMP FOR IMAGE RENAMING ONLY + // e.g., "2025-09-30" and "14:34:19" -> "20250930143419" + final String dateStr = (data.samplingDate ?? '').replaceAll('-', ''); + final String timeStr = (data.samplingTime ?? '').replaceAll(':', ''); + final String zipImageTimestamp = "$dateStr$timeStr"; + + // 2. USE ORIGINAL BASE FILENAME (Report ID / Milliseconds) for Folder/Zip final baseFileName = _generateBaseFileName(data); + // 3. SETUP DIRECTORIES final Directory? logDirectory = await _localStorageService.getRiverInSituBaseDir(data.samplingType, serverName: serverName); // Use correct base dir getter - // --- START: MODIFIED folderName --- // Use baseFileName for the folder name to match [stationCode]_[reportId] - final folderName = baseFileName; - // --- END: MODIFIED folderName --- - - final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, folderName)) : null; + final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, baseFileName)) : null; if (localSubmissionDir != null && !await localSubmissionDir.exists()) { await localSubmissionDir.create(recursive: true); } - // Create and upload data ZIP (with multiple JSON files specific to River In-Situ) + // 4. CREATE DATA ZIP final dataZip = await _zippingService.createDataZip( jsonDataMap: { 'db.json': data.toDbJson(), @@ -527,22 +551,48 @@ class RiverInSituSamplingService { baseFileName: baseFileName, destinationDir: localSubmissionDir, ); + Map ftpDataResult = {'success': true, 'statuses': []}; if (dataZip != null) { ftpDataResult = await _submissionFtpService.submit( moduleName: moduleName, fileToUpload: dataZip, remotePath: '/${p.basename(dataZip.path)}'); } - // Create and upload image ZIP - final imageZip = await _zippingService.createImageZip( - imageFiles: imageFiles.values.toList(), - baseFileName: baseFileName, - destinationDir: localSubmissionDir, - ); + // 5. CREATE IMAGE ZIP (RENAMING LOGIC) Map ftpImageResult = {'success': true, 'statuses': []}; - if (imageZip != null) { - ftpImageResult = await _submissionFtpService.submit( - moduleName: moduleName, fileToUpload: imageZip, remotePath: '/${p.basename(imageZip.path)}'); + + // Create map: "New Name Inside Zip" -> "Original File on Phone" + final Map imagesForZip = {}; + + void mapImage(File? file, String prefix) { + if (file != null && file.existsSync()) { + // Rename inside zip using the READABLE timestamp: prefix_20250930143419.jpg + imagesForZip['${prefix}_$zipImageTimestamp.jpg'] = file; + } + } + + mapImage(data.backgroundStationImage, 'background'); + mapImage(data.upstreamRiverImage, 'upstream'); + mapImage(data.downstreamRiverImage, 'downstream'); + mapImage(data.sampleTurbidityImage, 'turbidity'); + mapImage(data.optionalImage1, 'optional_1'); + mapImage(data.optionalImage2, 'optional_2'); + mapImage(data.optionalImage3, 'optional_3'); + mapImage(data.optionalImage4, 'optional_4'); + + if (imagesForZip.isNotEmpty) { + // Call the NEW function: createRenamedImageZip + // Zip file name still uses baseFileName (milliseconds) + final imageZip = await _zippingService.createRenamedImageZip( + imageFiles: imagesForZip, + baseFileName: baseFileName, + destinationDir: localSubmissionDir, + ); + + if (imageZip != null) { + ftpImageResult = await _submissionFtpService.submit( + moduleName: moduleName, fileToUpload: imageZip, remotePath: '/${p.basename(imageZip.path)}'); + } } return { diff --git a/lib/services/river_investigative_sampling_service.dart b/lib/services/river_investigative_sampling_service.dart index 9f2309b..493237e 100644 --- a/lib/services/river_investigative_sampling_service.dart +++ b/lib/services/river_investigative_sampling_service.dart @@ -379,19 +379,38 @@ class RiverInvestigativeSamplingService { // Renamed class } if (finalImageFiles.isNotEmpty) { - final imageZip = await _zippingService.createImageZip( - imageFiles: finalImageFiles.values.toList(), + // Use existing queue logic for fallback (no renaming complexity here to be safe) + final Map retryImages = {}; + final String dateStr = (data.samplingDate ?? '').replaceAll('-', ''); + final String timeStr = (data.samplingTime ?? '').replaceAll(':', ''); + final String zipImageTimestamp = "$dateStr$timeStr"; + + void addRetryMap(File? file, String prefix) { + if(file != null) retryImages['${prefix}_$zipImageTimestamp.jpg'] = file; + } + addRetryMap(data.backgroundStationImage, 'background'); + addRetryMap(data.upstreamRiverImage, 'upstream'); + addRetryMap(data.downstreamRiverImage, 'downstream'); + addRetryMap(data.sampleTurbidityImage, 'sample_turbidity'); + addRetryMap(data.optionalImage1, 'optional_1'); + addRetryMap(data.optionalImage2, 'optional_2'); + addRetryMap(data.optionalImage3, 'optional_3'); + addRetryMap(data.optionalImage4, 'optional_4'); + + final retryImageZip = await _zippingService.createRenamedImageZip( + imageFiles: retryImages, baseFileName: baseFileNameForQueue, - destinationDir: null, // Save to temp dir + destinationDir: null, ); - if (imageZip != null) { + + if (retryImageZip != null) { // Queue for each config separately for (final config in ftpConfigs) { final configId = config['ftp_config_id']; if (configId != null) { await _retryService.addFtpToQueue( - localFilePath: imageZip.path, - remotePath: '/${p.basename(imageZip.path)}', // Standard remote path + localFilePath: retryImageZip.path, + remotePath: '/${p.basename(retryImageZip.path)}', // Standard remote path ftpConfigId: configId // Provide the specific config ID ); } @@ -595,22 +614,26 @@ class RiverInvestigativeSamplingService { // Renamed class // --- END: MODIFIED _generateBaseFileName --- /// Generates data and image ZIP files and uploads them using SubmissionFtpService (Investigative). - Future> _generateAndUploadFtpFiles(RiverInvesManualSamplingData data, Map imageFiles, String serverName, String moduleName) async { // Updated model type + Future> _generateAndUploadFtpFiles(RiverInvesManualSamplingData data, Map imageFiles, String serverName, String moduleName) async { + + // 1. GENERATE TIMESTAMP FOR IMAGE RENAMING + // e.g., "2025-09-30" and "14:34:19" -> "20250930143419" + final String dateStr = (data.samplingDate ?? '').replaceAll('-', ''); + final String timeStr = (data.samplingTime ?? '').replaceAll(':', ''); + final String zipImageTimestamp = "$dateStr$timeStr"; + + // 2. USE ORIGINAL BASE FILENAME (Report ID / Milliseconds) for Folder/Zip final baseFileName = _generateBaseFileName(data); // Use helper - // *** MODIFIED: Use correct base dir getter *** + // 3. SETUP DIRECTORIES final Directory? logDirectory = await _localStorageService.getRiverInvestigativeBaseDir(serverName: serverName); // NEW GETTER - // Determine the specific folder for this submission log within the base directory - // --- START: MODIFIED folderName --- - final folderName = baseFileName; // Use the timestamp-based filename - // --- END: MODIFIED folderName --- - final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, folderName)) : null; + final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, baseFileName)) : null; if (localSubmissionDir != null && !await localSubmissionDir.exists()) { await localSubmissionDir.create(recursive: true); // Create if doesn't exist } - // Create and upload data ZIP (with multiple JSON files specific to River Investigative) + // 4. CREATE DATA ZIP final dataZip = await _zippingService.createDataZip( jsonDataMap: { // *** MODIFIED: Use Investigative model's JSON methods and filenames *** @@ -622,6 +645,7 @@ class RiverInvestigativeSamplingService { // Renamed class baseFileName: baseFileName, destinationDir: localSubmissionDir, // Save ZIP in the specific log folder ); + Map ftpDataResult = {'success': true, 'statuses': []}; // Default success if no file if (dataZip != null) { ftpDataResult = await _submissionFtpService.submit( @@ -631,14 +655,37 @@ class RiverInvestigativeSamplingService { // Renamed class ); } - // Create and upload image ZIP (if images exist) - Map ftpImageResult = {'success': true, 'statuses': []}; // Default success if no images - if (imageFiles.isNotEmpty) { - final imageZip = await _zippingService.createImageZip( - imageFiles: imageFiles.values.toList(), + // 5. CREATE IMAGE ZIP (RENAMING LOGIC) + Map ftpImageResult = {'success': true, 'statuses': []}; + + // Create mapping: "New Name Inside Zip" -> "Original File on Phone" + final Map imagesForZip = {}; + + void mapImage(File? file, String prefix) { + if (file != null && file.existsSync()) { + // Rename inside zip: prefix_20250930143419.jpg + imagesForZip['${prefix}_$zipImageTimestamp.jpg'] = file; + } + } + + // Map images (Investigative model uses same names as others) + mapImage(data.backgroundStationImage, 'background'); + mapImage(data.upstreamRiverImage, 'upstream'); + mapImage(data.downstreamRiverImage, 'downstream'); + mapImage(data.sampleTurbidityImage, 'turbidity'); + mapImage(data.optionalImage1, 'optional_1'); + mapImage(data.optionalImage2, 'optional_2'); + mapImage(data.optionalImage3, 'optional_3'); + mapImage(data.optionalImage4, 'optional_4'); + + if (imagesForZip.isNotEmpty) { + // *** MODIFICATION: Call the NEW renaming function *** + final imageZip = await _zippingService.createRenamedImageZip( + imageFiles: imagesForZip, baseFileName: baseFileName, destinationDir: localSubmissionDir, // Save ZIP in the specific log folder ); + if (imageZip != null) { ftpImageResult = await _submissionFtpService.submit( moduleName: moduleName, // 'river_investigative' @@ -648,7 +695,6 @@ class RiverInvestigativeSamplingService { // Renamed class } } - // Combine statuses from both uploads return { 'statuses': >[ diff --git a/lib/services/river_manual_triennial_sampling_service.dart b/lib/services/river_manual_triennial_sampling_service.dart index ba09089..6dbfd49 100644 --- a/lib/services/river_manual_triennial_sampling_service.dart +++ b/lib/services/river_manual_triennial_sampling_service.dart @@ -347,19 +347,39 @@ class RiverManualTriennialSamplingService { } if (finalImageFiles.isNotEmpty) { - final imageZip = await _zippingService.createImageZip( - imageFiles: finalImageFiles.values.toList(), + // Note: For the session expired case, renaming logic would ideally be here too, + // but requires complex reconstruction of the map. Following the previous pattern, + // we attempt to respect the rename if possible. + final Map retryImages = {}; + final String dateStr = (data.samplingDate ?? '').replaceAll('-', ''); + final String timeStr = (data.samplingTime ?? '').replaceAll(':', ''); + final String timestampId = "$dateStr$timeStr"; + + void addRetryMap(File? file, String prefix) { + if(file != null) retryImages['${prefix}_$timestampId.jpg'] = file; + } + addRetryMap(data.backgroundStationImage, 'background'); + addRetryMap(data.upstreamRiverImage, 'upstream'); + addRetryMap(data.downstreamRiverImage, 'downstream'); + addRetryMap(data.sampleTurbidityImage, 'sample_turbidity'); + addRetryMap(data.optionalImage1, 'optional_1'); + addRetryMap(data.optionalImage2, 'optional_2'); + addRetryMap(data.optionalImage3, 'optional_3'); + addRetryMap(data.optionalImage4, 'optional_4'); + + final retryImageZip = await _zippingService.createRenamedImageZip( + imageFiles: retryImages, baseFileName: baseFileNameForQueue, destinationDir: null, ); - if (imageZip != null) { + if (retryImageZip != null) { // Queue for each config separately for (final config in ftpConfigs) { final configId = config['ftp_config_id']; if (configId != null) { await _retryService.addFtpToQueue( - localFilePath: imageZip.path, - remotePath: '/${p.basename(imageZip.path)}', + localFilePath: retryImageZip.path, + remotePath: '/${p.basename(retryImageZip.path)}', ftpConfigId: configId // Provide the specific config ID ); } @@ -492,44 +512,77 @@ class RiverManualTriennialSamplingService { /// Generates data and image ZIP files and uploads them using SubmissionFtpService. Future> _generateAndUploadFtpFiles(RiverManualTriennialSamplingData data, Map imageFiles, String serverName, String moduleName) async { + + // 1. GENERATE TIMESTAMP FOR IMAGE RENAMING + // e.g., "2025-09-30" and "14:34:19" -> "20250930143419" + final String dateStr = (data.samplingDate ?? '').replaceAll('-', ''); + final String timeStr = (data.samplingTime ?? '').replaceAll(':', ''); + final String zipImageTimestamp = "$dateStr$timeStr"; + + // 2. USE ORIGINAL BASE FILENAME (Report ID / Milliseconds) for Folder/Zip final baseFileName = _generateBaseFileName(data); + // 3. SETUP DIRECTORIES final Directory? logDirectory = await _localStorageService.getLogDirectory( // Use generic getter serverName: serverName, module: 'river', subModule: 'river_triennial_sampling', // Correct sub-module path ); - // --- START: MODIFIED folderName --- - final folderName = baseFileName; // Use the timestamp-based filename - // --- END: MODIFIED folderName --- - final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, folderName)) : null; + // Use baseFileName for the folder + final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, baseFileName)) : null; if (localSubmissionDir != null && !await localSubmissionDir.exists()) { await localSubmissionDir.create(recursive: true); } - // Create and upload data ZIP + // 4. CREATE DATA ZIP final dataZip = await _zippingService.createDataZip( jsonDataMap: {'db.json': data.toDbJson()}, // Assuming similar structure, adjust if needed baseFileName: baseFileName, destinationDir: localSubmissionDir, ); + Map ftpDataResult = {'success': true, 'statuses': []}; if (dataZip != null) { ftpDataResult = await _submissionFtpService.submit( moduleName: moduleName, fileToUpload: dataZip, remotePath: '/${p.basename(dataZip.path)}'); } - // Create and upload image ZIP - final imageZip = await _zippingService.createImageZip( - imageFiles: imageFiles.values.toList(), - baseFileName: baseFileName, - destinationDir: localSubmissionDir, - ); + // 5. CREATE IMAGE ZIP (RENAMING LOGIC) Map ftpImageResult = {'success': true, 'statuses': []}; - if (imageZip != null) { - ftpImageResult = await _submissionFtpService.submit( - moduleName: moduleName, fileToUpload: imageZip, remotePath: '/${p.basename(imageZip.path)}'); + + // Create map: "New Name Inside Zip" -> "Original File on Phone" + final Map imagesForZip = {}; + + void mapImage(File? file, String prefix) { + if (file != null && file.existsSync()) { + // Rename inside zip: prefix_20250930143419.jpg + imagesForZip['${prefix}_$zipImageTimestamp.jpg'] = file; + } + } + + // Map the specific fields to their short prefixes + mapImage(data.backgroundStationImage, 'background'); + mapImage(data.upstreamRiverImage, 'upstream'); + mapImage(data.downstreamRiverImage, 'downstream'); + mapImage(data.sampleTurbidityImage, 'turbidity'); + mapImage(data.optionalImage1, 'optional_1'); + mapImage(data.optionalImage2, 'optional_2'); + mapImage(data.optionalImage3, 'optional_3'); + mapImage(data.optionalImage4, 'optional_4'); + + if (imagesForZip.isNotEmpty) { + // Call the NEW function: createRenamedImageZip + final imageZip = await _zippingService.createRenamedImageZip( + imageFiles: imagesForZip, + baseFileName: baseFileName, + destinationDir: localSubmissionDir, + ); + + if (imageZip != null) { + ftpImageResult = await _submissionFtpService.submit( + moduleName: moduleName, fileToUpload: imageZip, remotePath: '/${p.basename(imageZip.path)}'); + } } return { diff --git a/lib/services/zipping_service.dart b/lib/services/zipping_service.dart index 6d50d6d..f9b5940 100644 --- a/lib/services/zipping_service.dart +++ b/lib/services/zipping_service.dart @@ -1,15 +1,14 @@ +// lib/services/zipping_service.dart + import 'dart:io'; -import 'dart:convert'; // Added to ensure correct UTF-8 encoding +import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:archive/archive_io.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; -/// A dedicated service to handle the creation of ZIP archives for FTP submission. class ZippingService { /// Creates multiple JSON files from a map of data and zips them into a single archive. - /// The map keys will be the filenames (e.g., 'db.json', 'form_data.json'). - /// The map values should be the JSON string content for each file. Future createDataZip({ required Map jsonDataMap, required String baseFileName, @@ -17,7 +16,6 @@ class ZippingService { }) async { try { final targetDir = destinationDir ?? await getTemporaryDirectory(); - // Ensure the target directory exists before creating the file if (!await targetDir.exists()) { await targetDir.create(recursive: true); } @@ -30,18 +28,9 @@ class ZippingService { for (var entry in jsonDataMap.entries) { final fileName = entry.key; final jsonContent = entry.value; - - // --- MODIFIED: Ensure UTF-8 encoding --- - // 1. Encode the string content into UTF-8 bytes final utf8Bytes = utf8.encode(jsonContent); - - // 2. Use the UTF-8 bytes and their correct length for the archive - // (This replaces the original: jsonContent.length, jsonContent.codeUnits) final archiveFile = ArchiveFile(fileName, utf8Bytes.length, utf8Bytes); - // --- END MODIFICATION --- - encoder.addArchiveFile(archiveFile); - debugPrint("Added $fileName to data ZIP."); } @@ -54,11 +43,13 @@ class ZippingService { } } + /// [ORIGINAL FUNCTION RESTORED] /// Creates a ZIP file from a list of image files. + /// Used by Marine, Air, etc. that do not need renaming. Future createImageZip({ required List imageFiles, required String baseFileName, - Directory? destinationDir, // ADDED: New optional parameter + Directory? destinationDir, }) async { if (imageFiles.isEmpty) { debugPrint("No images provided to create an image ZIP."); @@ -67,7 +58,6 @@ class ZippingService { try { final targetDir = destinationDir ?? await getTemporaryDirectory(); - // Ensure the target directory exists before creating the file if (!await targetDir.exists()) { await targetDir.create(recursive: true); } @@ -94,4 +84,52 @@ class ZippingService { return null; } } + + /// [NEW FUNCTION FOR RIVER] + /// Creates a ZIP file from a Map of image files to allow specific renaming inside the ZIP. + /// Key: The filename to be used INSIDE the zip (e.g., 'background_20231213.jpg') + /// Value: The actual File object on the device. + Future createRenamedImageZip({ + required Map imageFiles, + required String baseFileName, + Directory? destinationDir, + }) async { + if (imageFiles.isEmpty) { + debugPrint("No images provided to create an image ZIP."); + return null; + } + + try { + final targetDir = destinationDir ?? await getTemporaryDirectory(); + if (!await targetDir.exists()) { + await targetDir.create(recursive: true); + } + final zipFilePath = p.join(targetDir.path, '${baseFileName}_img.zip'); + final encoder = ZipFileEncoder(); + encoder.create(zipFilePath); + + debugPrint("Creating renamed image ZIP at: $zipFilePath"); + + for (var entry in imageFiles.entries) { + final String targetName = entry.key; + final File sourceFile = entry.value; + + if (await sourceFile.exists()) { + final bytes = await sourceFile.readAsBytes(); + final archiveFile = ArchiveFile(targetName, bytes.length, bytes); + encoder.addArchiveFile(archiveFile); + debugPrint("Added ${p.basename(sourceFile.path)} as $targetName"); + } else { + debugPrint("Skipping non-existent file: ${sourceFile.path}"); + } + } + + encoder.close(); + debugPrint("Renamed Image ZIP creation complete."); + return File(zipFilePath); + } catch (e) { + debugPrint("Error creating renamed image ZIP file: $e"); + return null; + } + } } \ No newline at end of file