change river ftp folder name and image naming to follow mms 1.00 to edc

This commit is contained in:
ALim Aidrus 2025-11-19 14:21:10 +08:00
parent c543e82d5b
commit 18e853ac83
6 changed files with 264 additions and 77 deletions

View File

@ -29,7 +29,7 @@ class _HomePageState extends State<HomePage> {
});
},
),
title: const Text("MMS Version 3.8.01"),
title: const Text("MMS Version 3.12.01"),
actions: [
IconButton(
icon: const Icon(Icons.person),

View File

@ -221,7 +221,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
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(

View File

@ -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<String, File> 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<Map<String, dynamic>> _generateAndUploadFtpFiles(RiverInSituSamplingData data, Map<String, File> 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,23 +551,49 @@ class RiverInSituSamplingService {
baseFileName: baseFileName,
destinationDir: localSubmissionDir,
);
Map<String, dynamic> 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(),
// 5. CREATE IMAGE ZIP (RENAMING LOGIC)
Map<String, dynamic> ftpImageResult = {'success': true, 'statuses': []};
// Create map: "New Name Inside Zip" -> "Original File on Phone"
final Map<String, File> 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,
);
Map<String, dynamic> ftpImageResult = {'success': true, 'statuses': []};
if (imageZip != null) {
ftpImageResult = await _submissionFtpService.submit(
moduleName: moduleName, fileToUpload: imageZip, remotePath: '/${p.basename(imageZip.path)}');
}
}
return {
'statuses': <Map<String, dynamic>>[

View File

@ -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<String, File> 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<Map<String, dynamic>> _generateAndUploadFtpFiles(RiverInvesManualSamplingData data, Map<String, File> imageFiles, String serverName, String moduleName) async { // Updated model type
Future<Map<String, dynamic>> _generateAndUploadFtpFiles(RiverInvesManualSamplingData data, Map<String, File> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> ftpImageResult = {'success': true, 'statuses': []};
// Create mapping: "New Name Inside Zip" -> "Original File on Phone"
final Map<String, File> 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': <Map<String, dynamic>>[

View File

@ -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<String, File> 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,45 +512,78 @@ class RiverManualTriennialSamplingService {
/// Generates data and image ZIP files and uploads them using SubmissionFtpService.
Future<Map<String, dynamic>> _generateAndUploadFtpFiles(RiverManualTriennialSamplingData data, Map<String, File> 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<String, dynamic> 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(),
// 5. CREATE IMAGE ZIP (RENAMING LOGIC)
Map<String, dynamic> ftpImageResult = {'success': true, 'statuses': []};
// Create map: "New Name Inside Zip" -> "Original File on Phone"
final Map<String, File> 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,
);
Map<String, dynamic> ftpImageResult = {'success': true, 'statuses': []};
if (imageZip != null) {
ftpImageResult = await _submissionFtpService.submit(
moduleName: moduleName, fileToUpload: imageZip, remotePath: '/${p.basename(imageZip.path)}');
}
}
return {
'statuses': <Map<String, dynamic>>[

View File

@ -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<File?> createDataZip({
required Map<String, String> 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<File?> createImageZip({
required List<File> 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<File?> createRenamedImageZip({
required Map<String, File> 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;
}
}
}