modify ftp module to follow mms 1.0 format
This commit is contained in:
parent
629c582aa9
commit
c543e82d5b
@ -344,7 +344,7 @@ class InSituSamplingData {
|
|||||||
'remarks_lab': labRemarks,
|
'remarks_lab': labRemarks,
|
||||||
};
|
};
|
||||||
// Remove null values before encoding
|
// Remove null values before encoding
|
||||||
data.removeWhere((key, value) => value == null);
|
//data.removeWhere((key, value) => value == null);
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -354,7 +354,7 @@ class RiverInSituSamplingData {
|
|||||||
// --- END FIX ---
|
// --- END FIX ---
|
||||||
};
|
};
|
||||||
// Remove null values before encoding
|
// Remove null values before encoding
|
||||||
data.removeWhere((key, value) => value == null);
|
//data.removeWhere((key, value) => value == null);
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -482,7 +482,7 @@ class RiverInvesManualSamplingData {
|
|||||||
'new_river': stationTypeSelection == 'New Location' ? newRiverName : null,
|
'new_river': stationTypeSelection == 'New Location' ? newRiverName : null,
|
||||||
'new_station_name': stationTypeSelection == 'New Location' ? newStationName : null, // Include newStationName
|
'new_station_name': stationTypeSelection == 'New Location' ? newStationName : null, // Include newStationName
|
||||||
};
|
};
|
||||||
data.removeWhere((key, value) => value == null);
|
//data.removeWhere((key, value) => value == null);
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -331,7 +331,7 @@ class RiverManualTriennialSamplingData {
|
|||||||
'remarks_event': eventRemarks,
|
'remarks_event': eventRemarks,
|
||||||
'remarks_lab': labRemarks,
|
'remarks_lab': labRemarks,
|
||||||
};
|
};
|
||||||
data.removeWhere((key, value) => value == null);
|
//data.removeWhere((key, value) => value == null);
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,7 +187,7 @@ class TarballSamplingData {
|
|||||||
'longitude': currentLongitude, // Current location
|
'longitude': currentLongitude, // Current location
|
||||||
'sample_id': reportId, // Using reportId if available
|
'sample_id': reportId, // Using reportId if available
|
||||||
};
|
};
|
||||||
data.removeWhere((key, value) => value == null);
|
//data.removeWhere((key, value) => value == null);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,7 +53,7 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
List<String> _tarballStatesList = [];
|
List<String> _tarballStatesList = [];
|
||||||
List<Map<String, dynamic>> _stationsForTarballState = [];
|
List<Map<String, dynamic>> _stationsForTarballState = [];
|
||||||
|
|
||||||
final List<String> _samplingTypes = ['Schedule', 'Ad-Hoc', 'Complaint', 'Investigative'];
|
final List<String> _samplingTypes = ['Investigative'];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|||||||
@ -40,7 +40,7 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
List<String> _statesList = [];
|
List<String> _statesList = [];
|
||||||
List<String> _categoriesForState = [];
|
List<String> _categoriesForState = [];
|
||||||
List<Map<String, dynamic>> _stationsForCategory = [];
|
List<Map<String, dynamic>> _stationsForCategory = [];
|
||||||
final List<String> _samplingTypes = ['Schedule', 'Ad-Hoc', 'Complaint'];
|
final List<String> _samplingTypes = ['Schedule'];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|||||||
@ -64,7 +64,7 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
|||||||
if (file != null) {
|
if (file != null) {
|
||||||
setState(() => setImageCallback(file));
|
setState(() => setImageCallback(file));
|
||||||
} else if (mounted) {
|
} else if (mounted) {
|
||||||
_showSnackBar('Image selection failed. Please ensure all photos are taken in landscape (vertical) mode.', isError: true);
|
_showSnackBar('Image selection failed. Please ensure all photos are taken in landscape (horizontal) mode.', isError: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -143,7 +143,7 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
|||||||
// --- Section: Required Photos ---
|
// --- Section: Required Photos ---
|
||||||
Text("Required Photos *", style: Theme.of(context).textTheme.titleLarge),
|
Text("Required Photos *", style: Theme.of(context).textTheme.titleLarge),
|
||||||
const Text(
|
const Text(
|
||||||
"All photos must be in landscape (vertical) orientation. A watermark will be applied automatically.",
|
"All photos must be in landscape (horizontal) orientation. A watermark will be applied automatically.",
|
||||||
style: TextStyle(color: Colors.grey)
|
style: TextStyle(color: Colors.grey)
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
@ -409,7 +409,7 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
|
|
||||||
List<Map<String, dynamic>> _validateParameters(Map<String, double> readings, List<Map<String, dynamic>> limits) {
|
List<Map<String, dynamic>> _validateParameters(Map<String, double> readings, List<Map<String, dynamic>> limits) {
|
||||||
final List<Map<String, dynamic>> invalidParams = [];
|
final List<Map<String, dynamic>> invalidParams = [];
|
||||||
final int? stationId = widget.data.selectedStation?['man_station_id'];
|
final int? stationId = widget.data.selectedStation?['station_id'];
|
||||||
|
|
||||||
debugPrint("--- Parameter Validation Start ---");
|
debugPrint("--- Parameter Validation Start ---");
|
||||||
debugPrint("Selected Station ID: $stationId");
|
debugPrint("Selected Station ID: $stationId");
|
||||||
|
|||||||
@ -116,16 +116,42 @@ class AirSamplingService {
|
|||||||
return File(filePath)..writeAsBytesSync(img.encodeJpg(originalImage));
|
return File(filePath)..writeAsBytesSync(img.encodeJpg(originalImage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: MODIFIED FILENAME LOGIC ---
|
||||||
|
/// Generates a unique timestamp ID from the sampling date and time.
|
||||||
|
String _generateTimestampId(String? date, String? time) {
|
||||||
|
final String dateTimeString = "${date ?? ''} ${time ?? ''}";
|
||||||
|
try {
|
||||||
|
// Air time format seems to be 'HH:mm'
|
||||||
|
final DateTime samplingDateTime = DateFormat('yyyy-MM-dd HH:mm').parse(dateTimeString);
|
||||||
|
return samplingDateTime.millisecondsSinceEpoch.toString();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Could not parse '$dateTimeString' for timestamp ID, using current time. Error: $e");
|
||||||
|
return DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END: MODIFIED FILENAME LOGIC ---
|
||||||
|
|
||||||
// --- REFACTORED submitInstallation method with granular error handling ---
|
// --- REFACTORED submitInstallation method with granular error handling ---
|
||||||
Future<Map<String, dynamic>> submitInstallation(AirInstallationData data, List<Map<String, dynamic>>? appSettings) async {
|
Future<Map<String, dynamic>> submitInstallation(AirInstallationData data, List<Map<String, dynamic>>? appSettings) async {
|
||||||
const String moduleName = 'air_installation';
|
const String moduleName = 'air_installation';
|
||||||
final activeConfig = await _serverConfigService.getActiveApiConfig();
|
final activeConfig = await _serverConfigService.getActiveApiConfig();
|
||||||
final serverName = activeConfig?['config_name'] as String? ?? 'Default';
|
final serverName = activeConfig?['config_name'] as String? ?? 'Default';
|
||||||
|
|
||||||
|
// --- START: MODIFIED FILENAME LOGIC ---
|
||||||
|
// Generate the unique timestamp ID and base filename FIRST.
|
||||||
|
final String timestampId = _generateTimestampId(data.installationDate, data.installationTime);
|
||||||
|
final String stationCode = data.stationID ?? 'UNKNOWN';
|
||||||
|
final String baseFileName = "${stationCode}_INSTALLATION_${timestampId}";
|
||||||
|
|
||||||
|
// Assign this as the primary refID for the log
|
||||||
|
data.refID = baseFileName;
|
||||||
|
// --- END: MODIFIED FILENAME LOGIC ---
|
||||||
|
|
||||||
bool anyApiSuccess = false;
|
bool anyApiSuccess = false;
|
||||||
Map<String, dynamic> apiDataResult = {};
|
Map<String, dynamic> apiDataResult = {};
|
||||||
Map<String, dynamic> apiImageResult = {};
|
Map<String, dynamic> apiImageResult = {};
|
||||||
final imageFiles = data.getImagesForUpload();
|
final imageFiles = data.getImagesForUpload();
|
||||||
|
String? apiRecordId; // Will hold the DB ID (e.g., 102)
|
||||||
|
|
||||||
// Step 1: Attempt API Submission
|
// Step 1: Attempt API Submission
|
||||||
try {
|
try {
|
||||||
@ -136,13 +162,13 @@ class AirSamplingService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (apiDataResult['success'] == true) {
|
if (apiDataResult['success'] == true) {
|
||||||
final recordId = apiDataResult['data']?['air_man_id']?.toString();
|
apiRecordId = apiDataResult['data']?['air_man_id']?.toString();
|
||||||
if (recordId != null) {
|
if (apiRecordId != null) {
|
||||||
data.airManId = int.tryParse(recordId);
|
data.airManId = int.tryParse(apiRecordId); // Save the DB ID
|
||||||
apiImageResult = await _submissionApiService.submitMultipart(
|
apiImageResult = await _submissionApiService.submitMultipart(
|
||||||
moduleName: moduleName,
|
moduleName: moduleName,
|
||||||
endpoint: 'air/manual/installation-images',
|
endpoint: 'air/manual/installation-images',
|
||||||
fields: {'air_man_id': recordId},
|
fields: {'air_man_id': apiRecordId}, // Use DB ID for image upload
|
||||||
files: imageFiles,
|
files: imageFiles,
|
||||||
);
|
);
|
||||||
anyApiSuccess = apiImageResult['success'] == true;
|
anyApiSuccess = apiImageResult['success'] == true;
|
||||||
@ -170,9 +196,8 @@ class AirSamplingService {
|
|||||||
Map<String, dynamic> ftpImageResult = {'statuses': []};
|
Map<String, dynamic> ftpImageResult = {'statuses': []};
|
||||||
bool anyFtpSuccess = false;
|
bool anyFtpSuccess = false;
|
||||||
try {
|
try {
|
||||||
final stationCode = data.stationID ?? 'UNKNOWN';
|
// --- FILENAME LOGIC MOVED ---
|
||||||
final samplingDateTime = "${data.installationDate}_${data.installationTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
// baseFileName is already generated above.
|
||||||
final baseFileName = "${stationCode}_INSTALLATION_${samplingDateTime}";
|
|
||||||
|
|
||||||
final dataZip = await _zippingService.createDataZip(jsonDataMap: {'db.json': jsonEncode(data.toDbJson())}, baseFileName: baseFileName);
|
final dataZip = await _zippingService.createDataZip(jsonDataMap: {'db.json': jsonEncode(data.toDbJson())}, baseFileName: baseFileName);
|
||||||
if (dataZip != null) {
|
if (dataZip != null) {
|
||||||
@ -209,7 +234,20 @@ class AirSamplingService {
|
|||||||
finalMessage = 'Both API and FTP submissions failed and were queued.';
|
finalMessage = 'Both API and FTP submissions failed and were queued.';
|
||||||
}
|
}
|
||||||
|
|
||||||
await _logAndSave(data: data, status: finalStatus, message: finalMessage, apiResults: [apiDataResult, apiImageResult], ftpStatuses: [...ftpDataResult['statuses'], ...ftpImageResult['statuses']], serverName: serverName, type: 'Installation');
|
// --- START: MODIFIED LOGGING ---
|
||||||
|
await _logAndSave(
|
||||||
|
data: data,
|
||||||
|
status: finalStatus,
|
||||||
|
message: finalMessage,
|
||||||
|
apiResults: [apiDataResult, apiImageResult],
|
||||||
|
ftpStatuses: [...ftpDataResult['statuses'], ...ftpImageResult['statuses']],
|
||||||
|
serverName: serverName,
|
||||||
|
type: 'Installation',
|
||||||
|
baseFileName: baseFileName, // Pass the generated filename
|
||||||
|
apiRecordId: apiRecordId, // Pass the DB ID
|
||||||
|
);
|
||||||
|
// --- END: MODIFIED LOGGING ---
|
||||||
|
|
||||||
if (anyApiSuccess || anyFtpSuccess) {
|
if (anyApiSuccess || anyFtpSuccess) {
|
||||||
_handleInstallationSuccessAlert(data, appSettings, isDataOnly: imageFiles.isEmpty);
|
_handleInstallationSuccessAlert(data, appSettings, isDataOnly: imageFiles.isEmpty);
|
||||||
}
|
}
|
||||||
@ -223,14 +261,28 @@ class AirSamplingService {
|
|||||||
final activeConfig = await _serverConfigService.getActiveApiConfig();
|
final activeConfig = await _serverConfigService.getActiveApiConfig();
|
||||||
final serverName = activeConfig?['config_name'] as String? ?? 'Default';
|
final serverName = activeConfig?['config_name'] as String? ?? 'Default';
|
||||||
|
|
||||||
|
// --- START: MODIFIED FILENAME LOGIC ---
|
||||||
|
// Generate the unique timestamp ID and base filename FIRST.
|
||||||
|
final String timestampId = _generateTimestampId(data.collectionDate, data.collectionTime);
|
||||||
|
final String stationCode = installationData.stationID ?? 'UNKNOWN';
|
||||||
|
final String baseFileName = "${stationCode}_COLLECTION_${timestampId}";
|
||||||
|
|
||||||
|
// Assign this as the primary refID for the log
|
||||||
|
data.installationRefID = baseFileName;
|
||||||
|
// --- END: MODIFIED FILENAME LOGIC ---
|
||||||
|
|
||||||
bool anyApiSuccess = false;
|
bool anyApiSuccess = false;
|
||||||
Map<String, dynamic> apiDataResult = {};
|
Map<String, dynamic> apiDataResult = {};
|
||||||
Map<String, dynamic> apiImageResult = {};
|
Map<String, dynamic> apiImageResult = {};
|
||||||
final imageFiles = data.getImagesForUpload();
|
final imageFiles = data.getImagesForUpload();
|
||||||
|
String? apiRecordId; // Will hold the DB ID
|
||||||
|
|
||||||
// Step 1: Attempt API Submission
|
// Step 1: Attempt API Submission
|
||||||
try {
|
try {
|
||||||
|
// Use the DB ID from the original installation
|
||||||
data.airManId = installationData.airManId;
|
data.airManId = installationData.airManId;
|
||||||
|
apiRecordId = data.airManId?.toString(); // Store it for logging
|
||||||
|
|
||||||
apiDataResult = await _submissionApiService.submitPost(
|
apiDataResult = await _submissionApiService.submitPost(
|
||||||
moduleName: moduleName,
|
moduleName: moduleName,
|
||||||
endpoint: 'air/manual/collection',
|
endpoint: 'air/manual/collection',
|
||||||
@ -241,7 +293,7 @@ class AirSamplingService {
|
|||||||
apiImageResult = await _submissionApiService.submitMultipart(
|
apiImageResult = await _submissionApiService.submitMultipart(
|
||||||
moduleName: moduleName,
|
moduleName: moduleName,
|
||||||
endpoint: 'air/manual/collection-images',
|
endpoint: 'air/manual/collection-images',
|
||||||
fields: {'air_man_id': data.airManId.toString()},
|
fields: {'air_man_id': data.airManId.toString()}, // Use DB ID
|
||||||
files: imageFiles,
|
files: imageFiles,
|
||||||
);
|
);
|
||||||
anyApiSuccess = apiImageResult['success'] == true;
|
anyApiSuccess = apiImageResult['success'] == true;
|
||||||
@ -265,9 +317,8 @@ class AirSamplingService {
|
|||||||
Map<String, dynamic> ftpImageResult = {'statuses': []};
|
Map<String, dynamic> ftpImageResult = {'statuses': []};
|
||||||
bool anyFtpSuccess = false;
|
bool anyFtpSuccess = false;
|
||||||
try {
|
try {
|
||||||
final stationCode = installationData.stationID ?? 'UNKNOWN';
|
// --- FILENAME LOGIC MOVED ---
|
||||||
final samplingDateTime = "${data.collectionDate}_${data.collectionTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
// baseFileName is already generated above.
|
||||||
final baseFileName = "${stationCode}_COLLECTION_${samplingDateTime}";
|
|
||||||
|
|
||||||
final combinedJson = jsonEncode({"installation": installationData.toDbJson(), "collection": data.toMap()});
|
final combinedJson = jsonEncode({"installation": installationData.toDbJson(), "collection": data.toMap()});
|
||||||
final dataZip = await _zippingService.createDataZip(jsonDataMap: {'db.json': combinedJson}, baseFileName: baseFileName);
|
final dataZip = await _zippingService.createDataZip(jsonDataMap: {'db.json': combinedJson}, baseFileName: baseFileName);
|
||||||
@ -305,7 +356,21 @@ class AirSamplingService {
|
|||||||
finalMessage = 'Both API and FTP submissions failed and were queued.';
|
finalMessage = 'Both API and FTP submissions failed and were queued.';
|
||||||
}
|
}
|
||||||
|
|
||||||
await _logAndSave(data: data, installationData: installationData, status: finalStatus, message: finalMessage, apiResults: [apiDataResult, apiImageResult], ftpStatuses: [...ftpDataResult['statuses'], ...ftpImageResult['statuses']], serverName: serverName, type: 'Collection');
|
// --- START: MODIFIED LOGGING ---
|
||||||
|
await _logAndSave(
|
||||||
|
data: data,
|
||||||
|
installationData: installationData,
|
||||||
|
status: finalStatus,
|
||||||
|
message: finalMessage,
|
||||||
|
apiResults: [apiDataResult, apiImageResult],
|
||||||
|
ftpStatuses: [...ftpDataResult['statuses'], ...ftpImageResult['statuses']],
|
||||||
|
serverName: serverName,
|
||||||
|
type: 'Collection',
|
||||||
|
baseFileName: baseFileName, // Pass the generated filename
|
||||||
|
apiRecordId: apiRecordId, // Pass the DB ID
|
||||||
|
);
|
||||||
|
// --- END: MODIFIED LOGGING ---
|
||||||
|
|
||||||
if(anyApiSuccess || anyFtpSuccess) {
|
if(anyApiSuccess || anyFtpSuccess) {
|
||||||
_handleCollectionSuccessAlert(data, installationData, appSettings, isDataOnly: imageFiles.isEmpty);
|
_handleCollectionSuccessAlert(data, installationData, appSettings, isDataOnly: imageFiles.isEmpty);
|
||||||
}
|
}
|
||||||
@ -323,6 +388,10 @@ class AirSamplingService {
|
|||||||
required List<Map<String, dynamic>> ftpStatuses,
|
required List<Map<String, dynamic>> ftpStatuses,
|
||||||
required String serverName,
|
required String serverName,
|
||||||
required String type,
|
required String type,
|
||||||
|
// --- START: MODIFIED LOGGING ---
|
||||||
|
required String baseFileName, // Use this as the primary ID
|
||||||
|
String? apiRecordId, // The ID from the server DB (e.g., 102)
|
||||||
|
// --- END: MODIFIED LOGGING ---
|
||||||
}) async {
|
}) async {
|
||||||
String refID;
|
String refID;
|
||||||
Map<String, dynamic> formData;
|
Map<String, dynamic> formData;
|
||||||
@ -331,26 +400,28 @@ class AirSamplingService {
|
|||||||
if (type == 'Installation') {
|
if (type == 'Installation') {
|
||||||
final installation = data as AirInstallationData;
|
final installation = data as AirInstallationData;
|
||||||
installation.status = status;
|
installation.status = status;
|
||||||
refID = installation.refID!;
|
refID = installation.refID!; // This is now baseFileName
|
||||||
formData = installation.toMap();
|
formData = installation.toMap();
|
||||||
imagePaths = _getInstallationImagePaths(installation);
|
imagePaths = _getInstallationImagePaths(installation);
|
||||||
await _localStorageService.saveAirSamplingRecord(_toMapForLocalSave(installation), refID, serverName: serverName);
|
await _localStorageService.saveAirSamplingRecord(_toMapForLocalSave(installation), refID, serverName: serverName);
|
||||||
} else {
|
} else {
|
||||||
final collection = data as AirCollectionData;
|
final collection = data as AirCollectionData;
|
||||||
collection.status = status;
|
collection.status = status;
|
||||||
refID = collection.installationRefID!;
|
refID = collection.installationRefID!; // This is now baseFileName
|
||||||
formData = collection.toMap();
|
formData = collection.toMap();
|
||||||
imagePaths = _getCollectionImagePaths(collection);
|
imagePaths = _getCollectionImagePaths(collection);
|
||||||
await _localStorageService.saveAirSamplingRecord(_toMapForLocalSave(installationData!..collectionData = collection), refID, serverName: serverName);
|
await _localStorageService.saveAirSamplingRecord(_toMapForLocalSave(installationData!..collectionData = collection), refID, serverName: serverName);
|
||||||
}
|
}
|
||||||
|
|
||||||
final logData = {
|
final logData = {
|
||||||
'submission_id': refID,
|
// --- START: MODIFIED LOGGING ---
|
||||||
|
'submission_id': baseFileName, // Use the timestamp-based filename as the primary ID
|
||||||
'module': 'air',
|
'module': 'air',
|
||||||
'type': type,
|
'type': type,
|
||||||
'status': status,
|
'status': status,
|
||||||
'message': message,
|
'message': message,
|
||||||
'report_id': (data.airManId ?? installationData?.airManId)?.toString(),
|
'report_id': apiRecordId, // Store the server DB ID (e.g., 102) here
|
||||||
|
// --- END: MODIFIED LOGGING ---
|
||||||
'created_at': DateTime.now().toIso8601String(),
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
'form_data': jsonEncode(formData),
|
'form_data': jsonEncode(formData),
|
||||||
'image_data': jsonEncode(imagePaths),
|
'image_data': jsonEncode(imagePaths),
|
||||||
|
|||||||
@ -339,7 +339,7 @@ class LocalStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- START: MODIFICATION (FIXED ERROR) ---
|
// --- START: MODIFICATION (FIXED ERROR) ---
|
||||||
// Changed data.toDbJson() to data.toMap() to get a Map, not a String.
|
// This line is CORRECT. It uses data.toMap() to get a Map.
|
||||||
final Map<String, dynamic> jsonData = data.toMap();
|
final Map<String, dynamic> jsonData = data.toMap();
|
||||||
// --- END: MODIFICATION (FIXED ERROR) ---
|
// --- END: MODIFICATION (FIXED ERROR) ---
|
||||||
|
|
||||||
|
|||||||
@ -164,6 +164,22 @@ class MarineInSituSamplingService {
|
|||||||
_serialManager.dispose();
|
_serialManager.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: NEW HELPER METHOD ---
|
||||||
|
/// Generates a unique timestamp ID from the sampling date and time.
|
||||||
|
String _generateTimestampId(String? date, String? time) {
|
||||||
|
final String dateTimeString = "${date ?? ''} ${time ?? ''}";
|
||||||
|
try {
|
||||||
|
// Time format from model is HH:mm
|
||||||
|
final DateTime samplingDateTime = DateFormat('yyyy-MM-dd HH:mm').parse(dateTimeString);
|
||||||
|
return samplingDateTime.millisecondsSinceEpoch.toString();
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback: if parsing fails, use the current time in milliseconds
|
||||||
|
debugPrint("Could not parse '$dateTimeString' for timestamp ID, using current time. Error: $e");
|
||||||
|
return DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END: NEW HELPER METHOD ---
|
||||||
|
|
||||||
Future<Map<String, dynamic>> submitInSituSample({
|
Future<Map<String, dynamic>> submitInSituSample({
|
||||||
required InSituSamplingData data,
|
required InSituSamplingData data,
|
||||||
required List<Map<String, dynamic>>? appSettings,
|
required List<Map<String, dynamic>>? appSettings,
|
||||||
@ -173,6 +189,12 @@ class MarineInSituSamplingService {
|
|||||||
}) async {
|
}) async {
|
||||||
const String moduleName = 'marine_in_situ';
|
const String moduleName = 'marine_in_situ';
|
||||||
|
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// Generate the unique timestamp ID and assign it immediately.
|
||||||
|
final String timestampId = _generateTimestampId(data.samplingDate, data.samplingTime);
|
||||||
|
data.reportId = timestampId; // This is the primary ID now.
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
final connectivityResult = await Connectivity().checkConnectivity();
|
final connectivityResult = await Connectivity().checkConnectivity();
|
||||||
bool isOnline = !connectivityResult.contains(ConnectivityResult.none);
|
bool isOnline = !connectivityResult.contains(ConnectivityResult.none);
|
||||||
bool isOfflineSession = authProvider.isLoggedIn && (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false);
|
bool isOfflineSession = authProvider.isLoggedIn && (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false);
|
||||||
@ -235,6 +257,11 @@ class MarineInSituSamplingService {
|
|||||||
String finalStatus = '';
|
String finalStatus = '';
|
||||||
bool isSessionKnownToBeExpired = false;
|
bool isSessionKnownToBeExpired = false;
|
||||||
|
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
String? apiRecordId; // Will hold the DB ID (e.g., 102) from the server
|
||||||
|
// data.reportId already contains the timestamp ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Submit Form Data
|
// 1. Submit Form Data
|
||||||
apiDataResult = await _submissionApiService.submitPost(
|
apiDataResult = await _submissionApiService.submitPost(
|
||||||
@ -245,15 +272,20 @@ class MarineInSituSamplingService {
|
|||||||
|
|
||||||
if (apiDataResult['success'] == true) {
|
if (apiDataResult['success'] == true) {
|
||||||
anyApiSuccess = true;
|
anyApiSuccess = true;
|
||||||
data.reportId = apiDataResult['data']?['man_id']?.toString(); // Correct ID key for In-Situ
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// Store the server's database ID in a separate variable.
|
||||||
|
apiRecordId = apiDataResult['data']?['man_id']?.toString(); // Correct ID key for In-Situ
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
if (data.reportId != null) {
|
if (apiRecordId != null) {
|
||||||
if (finalImageFiles.isNotEmpty) {
|
if (finalImageFiles.isNotEmpty) {
|
||||||
// 2. Submit Images
|
// 2. Submit Images
|
||||||
apiImageResult = await _submissionApiService.submitMultipart(
|
apiImageResult = await _submissionApiService.submitMultipart(
|
||||||
moduleName: moduleName,
|
moduleName: moduleName,
|
||||||
endpoint: 'marine/manual/images', // Correct endpoint for In-Situ images
|
endpoint: 'marine/manual/images', // Correct endpoint for In-Situ images
|
||||||
fields: {'man_id': data.reportId!}, // Correct field key for In-Situ
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
fields: {'man_id': apiRecordId}, // Correct field key for In-Situ
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
files: finalImageFiles,
|
files: finalImageFiles,
|
||||||
);
|
);
|
||||||
if (apiImageResult['success'] != true) {
|
if (apiImageResult['success'] != true) {
|
||||||
@ -262,7 +294,9 @@ class MarineInSituSamplingService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
anyApiSuccess = false;
|
anyApiSuccess = false;
|
||||||
apiDataResult['message'] = 'API Error: Submission succeeded but did not return a record ID.';
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
apiDataResult['message'] = 'API Error: Submission succeeded but did not return a server record ID.';
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If apiDataResult['success'] is false, SubmissionApiService queued it.
|
// If apiDataResult['success'] is false, SubmissionApiService queued it.
|
||||||
@ -274,10 +308,12 @@ class MarineInSituSamplingService {
|
|||||||
apiDataResult = {'success': false, 'message': 'Session expired and re-login failed. API submission queued.'};
|
apiDataResult = {'success': false, 'message': 'Session expired and re-login failed. API submission queued.'};
|
||||||
// Manually queue API calls
|
// Manually queue API calls
|
||||||
await _retryService.addApiToQueue(endpoint: 'marine/manual/sample', method: 'POST', body: data.toApiFormData());
|
await _retryService.addApiToQueue(endpoint: 'marine/manual/sample', method: 'POST', body: data.toApiFormData());
|
||||||
if (finalImageFiles.isNotEmpty && data.reportId != null) {
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
if (finalImageFiles.isNotEmpty && apiRecordId != null) {
|
||||||
// Also queue images if data call might have partially succeeded before expiry
|
// Also queue images if data call might have partially succeeded before expiry
|
||||||
await _retryService.addApiToQueue(endpoint: 'marine/manual/images', method: 'POST_MULTIPART', fields: {'man_id': data.reportId!}, files: finalImageFiles);
|
await _retryService.addApiToQueue(endpoint: 'marine/manual/images', method: 'POST_MULTIPART', fields: {'man_id': apiRecordId}, files: finalImageFiles);
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Submit FTP Files
|
// 3. Submit FTP Files
|
||||||
@ -286,7 +322,9 @@ class MarineInSituSamplingService {
|
|||||||
|
|
||||||
if (isSessionKnownToBeExpired) {
|
if (isSessionKnownToBeExpired) {
|
||||||
debugPrint("Skipping FTP attempt due to known expired session. Manually queuing FTP tasks.");
|
debugPrint("Skipping FTP attempt due to known expired session. Manually queuing FTP tasks.");
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
final baseFileNameForQueue = _generateBaseFileName(data); // Use helper
|
final baseFileNameForQueue = _generateBaseFileName(data); // Use helper
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
// --- START FIX: Add ftpConfigId when queuing ---
|
// --- START FIX: Add ftpConfigId when queuing ---
|
||||||
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
||||||
@ -377,6 +415,9 @@ class MarineInSituSamplingService {
|
|||||||
ftpStatuses: ftpResults['statuses'],
|
ftpStatuses: ftpResults['statuses'],
|
||||||
serverName: serverName,
|
serverName: serverName,
|
||||||
finalImageFiles: finalImageFiles, // Pass the map of actual files
|
finalImageFiles: finalImageFiles, // Pass the map of actual files
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
apiRecordId: apiRecordId, // Pass the server DB ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
logDirectory: logDirectory,
|
logDirectory: logDirectory,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -388,7 +429,7 @@ class MarineInSituSamplingService {
|
|||||||
}
|
}
|
||||||
// --- END FIX ---
|
// --- END FIX ---
|
||||||
|
|
||||||
return {'success': overallSuccess, 'message': finalMessage, 'reportId': data.reportId};
|
return {'success': overallSuccess, 'message': finalMessage, 'reportId': data.reportId}; // Return timestamp ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -428,8 +469,10 @@ class MarineInSituSamplingService {
|
|||||||
|
|
||||||
if (savedLogPath == null) {
|
if (savedLogPath == null) {
|
||||||
const message = "Failed to save submission to local device storage.";
|
const message = "Failed to save submission to local device storage.";
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
// Use empty map for finalImageFiles as saving failed
|
// Use empty map for finalImageFiles as saving failed
|
||||||
await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}, logDirectory: logDirectory);
|
await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}, apiRecordId: null, logDirectory: logDirectory);
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
return {'success': false, 'message': message};
|
return {'success': false, 'message': message};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -445,9 +488,9 @@ class MarineInSituSamplingService {
|
|||||||
|
|
||||||
const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
|
const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
|
||||||
// Log final queued state to central DB
|
// Log final queued state to central DB
|
||||||
// await _logAndSave(data: data, status: 'Queued', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}, logDirectory: savedLogPath);
|
// await _logAndSave(data: data, status: 'Queued', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}, apiRecordId: null, logDirectory: savedLogPath);
|
||||||
|
|
||||||
return {'success': true, 'message': successMessage, 'reportId': null}; // No report ID yet
|
return {'success': true, 'message': successMessage, 'reportId': data.reportId}; // Return timestamp ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: MODIFIED _generateBaseFileName ---
|
// --- START: MODIFIED _generateBaseFileName ---
|
||||||
@ -455,14 +498,13 @@ class MarineInSituSamplingService {
|
|||||||
String _generateBaseFileName(InSituSamplingData data) {
|
String _generateBaseFileName(InSituSamplingData data) {
|
||||||
final stationCode = data.selectedStation?['man_station_code'] ?? 'NA';
|
final stationCode = data.selectedStation?['man_station_code'] ?? 'NA';
|
||||||
|
|
||||||
// Check if reportId (timestamp) is available.
|
// We now always use data.reportId, which we set as the timestamp.
|
||||||
if (data.reportId != null && data.reportId!.isNotEmpty) {
|
if (data.reportId == null || data.reportId!.isEmpty) {
|
||||||
return '${stationCode}_${data.reportId}';
|
// This is a safety fallback, but should not happen if submitData is used.
|
||||||
} else {
|
debugPrint("Warning: reportId is null in _generateBaseFileName. Using current timestamp.");
|
||||||
// Fallback to old method if reportId is not available (e.g., offline queue)
|
return '${stationCode}_${DateTime.now().millisecondsSinceEpoch.toString()}';
|
||||||
final fileTimestamp = "${data.samplingDate}_${data.samplingTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
|
||||||
return '${stationCode}_$fileTimestamp';
|
|
||||||
}
|
}
|
||||||
|
return '${stationCode}_${data.reportId}';
|
||||||
}
|
}
|
||||||
// --- END: MODIFIED _generateBaseFileName ---
|
// --- END: MODIFIED _generateBaseFileName ---
|
||||||
|
|
||||||
@ -542,6 +584,9 @@ class MarineInSituSamplingService {
|
|||||||
required List<Map<String, dynamic>> ftpStatuses,
|
required List<Map<String, dynamic>> ftpStatuses,
|
||||||
required String serverName,
|
required String serverName,
|
||||||
required Map<String, File> finalImageFiles, // Changed to Map<String, File>
|
required Map<String, File> finalImageFiles, // Changed to Map<String, File>
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
String? apiRecordId, // This is the server DB ID (e.g., 102)
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
String? logDirectory,
|
String? logDirectory,
|
||||||
}) async {
|
}) async {
|
||||||
data.submissionStatus = status;
|
data.submissionStatus = status;
|
||||||
@ -560,7 +605,10 @@ class MarineInSituSamplingService {
|
|||||||
// Add submission metadata
|
// Add submission metadata
|
||||||
logMapData['submissionStatus'] = status;
|
logMapData['submissionStatus'] = status;
|
||||||
logMapData['submissionMessage'] = message;
|
logMapData['submissionMessage'] = message;
|
||||||
logMapData['reportId'] = data.reportId;
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// data.reportId (the timestamp) is already in the map from toMap()
|
||||||
|
logMapData['apiRecordId'] = apiRecordId; // Add the server DB ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
logMapData['serverConfigName'] = serverName;
|
logMapData['serverConfigName'] = serverName;
|
||||||
logMapData['api_status'] = jsonEncode(apiResults.where((r) => r.isNotEmpty).toList());
|
logMapData['api_status'] = jsonEncode(apiResults.where((r) => r.isNotEmpty).toList());
|
||||||
logMapData['ftp_status'] = jsonEncode(ftpStatuses);
|
logMapData['ftp_status'] = jsonEncode(ftpStatuses);
|
||||||
@ -577,12 +625,14 @@ class MarineInSituSamplingService {
|
|||||||
|
|
||||||
// Save to central DB log
|
// Save to central DB log
|
||||||
final logData = {
|
final logData = {
|
||||||
'submission_id': data.reportId ?? baseFileName, // Use helper result
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
'submission_id': data.reportId ?? baseFileName, // This is the timestamp ID
|
||||||
'module': 'marine',
|
'module': 'marine',
|
||||||
'type': 'In-Situ', // Correct type
|
'type': 'In-Situ', // Correct type
|
||||||
'status': status,
|
'status': status,
|
||||||
'message': message,
|
'message': message,
|
||||||
'report_id': data.reportId,
|
'report_id': apiRecordId, // This is the server DB ID (e.g., 102)
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
'created_at': DateTime.now().toIso8601String(),
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
'form_data': jsonEncode(logMapData), // Log the comprehensive map with paths
|
'form_data': jsonEncode(logMapData), // Log the comprehensive map with paths
|
||||||
'image_data': jsonEncode(finalImageFiles.values.map((f) => f.path).toList()), // List of paths for files actually submitted/zipped
|
'image_data': jsonEncode(finalImageFiles.values.map((f) => f.path).toList()), // List of paths for files actually submitted/zipped
|
||||||
|
|||||||
@ -171,6 +171,22 @@ class MarineInvestigativeSamplingService {
|
|||||||
_serialManager.dispose();
|
_serialManager.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: NEW HELPER METHOD ---
|
||||||
|
/// Generates a unique timestamp ID from the sampling date and time.
|
||||||
|
String _generateTimestampId(String? date, String? time) {
|
||||||
|
final String dateTimeString = "${date ?? ''} ${time ?? ''}";
|
||||||
|
try {
|
||||||
|
// Time format from model is HH:mm
|
||||||
|
final DateTime samplingDateTime = DateFormat('yyyy-MM-dd HH:mm').parse(dateTimeString);
|
||||||
|
return samplingDateTime.millisecondsSinceEpoch.toString();
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback: if parsing fails, use the current time in milliseconds
|
||||||
|
debugPrint("Could not parse '$dateTimeString' for timestamp ID, using current time. Error: $e");
|
||||||
|
return DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END: NEW HELPER METHOD ---
|
||||||
|
|
||||||
Future<Map<String, dynamic>> submitInvestigativeSample({
|
Future<Map<String, dynamic>> submitInvestigativeSample({
|
||||||
required MarineInvesManualSamplingData data,
|
required MarineInvesManualSamplingData data,
|
||||||
required List<Map<String, dynamic>>? appSettings,
|
required List<Map<String, dynamic>>? appSettings,
|
||||||
@ -180,6 +196,12 @@ class MarineInvestigativeSamplingService {
|
|||||||
}) async {
|
}) async {
|
||||||
const String moduleName = 'marine_investigative';
|
const String moduleName = 'marine_investigative';
|
||||||
|
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// Generate the unique timestamp ID and assign it immediately.
|
||||||
|
final String timestampId = _generateTimestampId(data.samplingDate, data.samplingTime);
|
||||||
|
data.reportId = timestampId; // This is the primary ID now.
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
final connectivityResult = await Connectivity().checkConnectivity();
|
final connectivityResult = await Connectivity().checkConnectivity();
|
||||||
bool isOnline = !connectivityResult.contains(ConnectivityResult.none);
|
bool isOnline = !connectivityResult.contains(ConnectivityResult.none);
|
||||||
bool isOfflineSession = authProvider.isLoggedIn && (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false);
|
bool isOfflineSession = authProvider.isLoggedIn && (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false);
|
||||||
@ -241,6 +263,11 @@ class MarineInvestigativeSamplingService {
|
|||||||
String finalStatus = '';
|
String finalStatus = '';
|
||||||
bool isSessionKnownToBeExpired = false;
|
bool isSessionKnownToBeExpired = false;
|
||||||
|
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
String? apiRecordId; // Will hold the DB ID (e.g., 102) from the server
|
||||||
|
// data.reportId already contains the timestamp ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Submit Form Data
|
// 1. Submit Form Data
|
||||||
apiDataResult = await _submissionApiService.submitPost(
|
apiDataResult = await _submissionApiService.submitPost(
|
||||||
@ -251,15 +278,20 @@ class MarineInvestigativeSamplingService {
|
|||||||
|
|
||||||
if (apiDataResult['success'] == true) {
|
if (apiDataResult['success'] == true) {
|
||||||
anyApiSuccess = true;
|
anyApiSuccess = true;
|
||||||
data.reportId = apiDataResult['data']?['man_inves_id']?.toString();
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// Store the server's database ID in a separate variable.
|
||||||
|
apiRecordId = apiDataResult['data']?['man_inves_id']?.toString();
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
if (data.reportId != null) {
|
if (apiRecordId != null) {
|
||||||
if (finalImageFiles.isNotEmpty) {
|
if (finalImageFiles.isNotEmpty) {
|
||||||
// 2. Submit Images
|
// 2. Submit Images
|
||||||
apiImageResult = await _submissionApiService.submitMultipart(
|
apiImageResult = await _submissionApiService.submitMultipart(
|
||||||
moduleName: moduleName,
|
moduleName: moduleName,
|
||||||
endpoint: 'marine-investigative/images',
|
endpoint: 'marine-investigative/images',
|
||||||
fields: {'man_inves_id': data.reportId!},
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
fields: {'man_inves_id': apiRecordId}, // Use server's ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
files: finalImageFiles,
|
files: finalImageFiles,
|
||||||
);
|
);
|
||||||
if (apiImageResult['success'] != true) {
|
if (apiImageResult['success'] != true) {
|
||||||
@ -268,7 +300,9 @@ class MarineInvestigativeSamplingService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
anyApiSuccess = false;
|
anyApiSuccess = false;
|
||||||
apiDataResult['message'] = 'API Error: Submission succeeded but did not return a record ID.';
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
apiDataResult['message'] = 'API Error: Submission succeeded but did not return a server record ID.';
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on SessionExpiredException catch (_) {
|
} on SessionExpiredException catch (_) {
|
||||||
@ -278,10 +312,12 @@ class MarineInvestigativeSamplingService {
|
|||||||
apiDataResult = {'success': false, 'message': 'Session expired and re-login failed. API submission queued.'};
|
apiDataResult = {'success': false, 'message': 'Session expired and re-login failed. API submission queued.'};
|
||||||
// Manually queue the API call since SubmissionApiService was never called or failed internally due to session
|
// Manually queue the API call since SubmissionApiService was never called or failed internally due to session
|
||||||
await _retryService.addApiToQueue(endpoint: 'marine-investigative/sample', method: 'POST', body: data.toApiFormData());
|
await _retryService.addApiToQueue(endpoint: 'marine-investigative/sample', method: 'POST', body: data.toApiFormData());
|
||||||
if (finalImageFiles.isNotEmpty && data.reportId != null) {
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
if (finalImageFiles.isNotEmpty && apiRecordId != null) {
|
||||||
// Also queue images if data call might have partially succeeded before expiry
|
// Also queue images if data call might have partially succeeded before expiry
|
||||||
await _retryService.addApiToQueue(endpoint: 'marine-investigative/images', method: 'POST_MULTIPART', fields: {'man_inves_id': data.reportId!}, files: finalImageFiles);
|
await _retryService.addApiToQueue(endpoint: 'marine-investigative/images', method: 'POST_MULTIPART', fields: {'man_inves_id': apiRecordId}, files: finalImageFiles);
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
}
|
}
|
||||||
// We no longer catch SocketException or TimeoutException here.
|
// We no longer catch SocketException or TimeoutException here.
|
||||||
|
|
||||||
@ -291,7 +327,9 @@ class MarineInvestigativeSamplingService {
|
|||||||
|
|
||||||
if (isSessionKnownToBeExpired) {
|
if (isSessionKnownToBeExpired) {
|
||||||
debugPrint("Skipping FTP attempt due to known expired session. Manually queuing FTP tasks.");
|
debugPrint("Skipping FTP attempt due to known expired session. Manually queuing FTP tasks.");
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
final baseFileNameForQueue = _generateBaseFileName(data);
|
final baseFileNameForQueue = _generateBaseFileName(data);
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
||||||
|
|
||||||
@ -372,6 +410,9 @@ class MarineInvestigativeSamplingService {
|
|||||||
ftpStatuses: ftpResults['statuses'],
|
ftpStatuses: ftpResults['statuses'],
|
||||||
serverName: serverName,
|
serverName: serverName,
|
||||||
finalImageFiles: finalImageFiles,
|
finalImageFiles: finalImageFiles,
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
apiRecordId: apiRecordId, // Pass the server DB ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
logDirectory: logDirectory,
|
logDirectory: logDirectory,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -383,7 +424,7 @@ class MarineInvestigativeSamplingService {
|
|||||||
_handleInvestigativeSuccessAlert(data, appSettings, isDataOnly: finalImageFiles.isEmpty, isSessionExpired: isSessionKnownToBeExpired);
|
_handleInvestigativeSuccessAlert(data, appSettings, isDataOnly: finalImageFiles.isEmpty, isSessionExpired: isSessionKnownToBeExpired);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {'success': overallSuccess, 'message': finalMessage, 'reportId': data.reportId};
|
return {'success': overallSuccess, 'message': finalMessage, 'reportId': data.reportId}; // Return timestamp ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -416,7 +457,9 @@ class MarineInvestigativeSamplingService {
|
|||||||
|
|
||||||
if (savedLogPath == null) {
|
if (savedLogPath == null) {
|
||||||
const message = "Failed to save submission to local device storage.";
|
const message = "Failed to save submission to local device storage.";
|
||||||
await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}, logDirectory: logDirectory);
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}, apiRecordId: null, logDirectory: logDirectory);
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
return {'success': false, 'message': message};
|
return {'success': false, 'message': message};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -430,9 +473,10 @@ class MarineInvestigativeSamplingService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
|
const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
|
||||||
return {'success': true, 'message': successMessage, 'reportId': null}; // No report ID yet
|
return {'success': true, 'message': successMessage, 'reportId': data.reportId}; // Return timestamp ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: MODIFIED _generateBaseFileName ---
|
||||||
String _generateBaseFileName(MarineInvesManualSamplingData data) {
|
String _generateBaseFileName(MarineInvesManualSamplingData data) {
|
||||||
String stationCode = 'NA';
|
String stationCode = 'NA';
|
||||||
if (data.stationTypeSelection == 'Existing Manual Station') {
|
if (data.stationTypeSelection == 'Existing Manual Station') {
|
||||||
@ -443,16 +487,15 @@ class MarineInvestigativeSamplingService {
|
|||||||
stationCode = data.newStationCode ?? 'NEW_NA';
|
stationCode = data.newStationCode ?? 'NEW_NA';
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: MODIFIED (from in-situ) ---
|
// We now always use data.reportId, which we set as the timestamp.
|
||||||
// Use reportId if available, otherwise fall back to timestamp
|
if (data.reportId == null || data.reportId!.isEmpty) {
|
||||||
if (data.reportId != null && data.reportId!.isNotEmpty) {
|
// This is a safety fallback, but should not happen.
|
||||||
return '${stationCode}_${data.reportId}';
|
debugPrint("Warning: reportId is null in _generateBaseFileName. Using current timestamp.");
|
||||||
} else {
|
return '${stationCode}_${DateTime.now().millisecondsSinceEpoch.toString()}';
|
||||||
final fileTimestamp = "${data.samplingDate}_${data.samplingTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
|
||||||
return '${stationCode}_$fileTimestamp';
|
|
||||||
}
|
}
|
||||||
// --- END: MODIFIED ---
|
return '${stationCode}_${data.reportId}';
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED _generateBaseFileName ---
|
||||||
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> _generateAndUploadFtpFiles(MarineInvesManualSamplingData data, Map<String, File> imageFiles, String serverName, String moduleName) async {
|
Future<Map<String, dynamic>> _generateAndUploadFtpFiles(MarineInvesManualSamplingData data, Map<String, File> imageFiles, String serverName, String moduleName) async {
|
||||||
@ -532,11 +575,14 @@ class MarineInvestigativeSamplingService {
|
|||||||
required List<Map<String, dynamic>> ftpStatuses,
|
required List<Map<String, dynamic>> ftpStatuses,
|
||||||
required String serverName,
|
required String serverName,
|
||||||
required Map<String, File> finalImageFiles, // Use final images map
|
required Map<String, File> finalImageFiles, // Use final images map
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
String? apiRecordId, // The server's DB ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
String? logDirectory, // Existing log directory path if updating
|
String? logDirectory, // Existing log directory path if updating
|
||||||
}) async {
|
}) async {
|
||||||
data.submissionStatus = status;
|
data.submissionStatus = status;
|
||||||
data.submissionMessage = message;
|
data.submissionMessage = message;
|
||||||
final baseFileName = _generateBaseFileName(data);
|
final baseFileName = _generateBaseFileName(data); // This now uses the timestamp ID
|
||||||
|
|
||||||
Map<String, dynamic> logMapData = data.toDbJson();
|
Map<String, dynamic> logMapData = data.toDbJson();
|
||||||
final imageFileMap = data.toApiImageFiles();
|
final imageFileMap = data.toApiImageFiles();
|
||||||
@ -545,7 +591,10 @@ class MarineInvestigativeSamplingService {
|
|||||||
});
|
});
|
||||||
logMapData['submissionStatus'] = status;
|
logMapData['submissionStatus'] = status;
|
||||||
logMapData['submissionMessage'] = message;
|
logMapData['submissionMessage'] = message;
|
||||||
logMapData['reportId'] = data.reportId;
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// data.reportId (the timestamp) is already in the map from toDbJson()
|
||||||
|
logMapData['apiRecordId'] = apiRecordId; // Add the server DB ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
logMapData['serverConfigName'] = serverName;
|
logMapData['serverConfigName'] = serverName;
|
||||||
logMapData['api_status'] = jsonEncode(apiResults.where((r) => r.isNotEmpty).toList());
|
logMapData['api_status'] = jsonEncode(apiResults.where((r) => r.isNotEmpty).toList());
|
||||||
logMapData['ftp_status'] = jsonEncode(ftpStatuses);
|
logMapData['ftp_status'] = jsonEncode(ftpStatuses);
|
||||||
@ -559,12 +608,14 @@ class MarineInvestigativeSamplingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final logData = {
|
final logData = {
|
||||||
'submission_id': data.reportId ?? baseFileName,
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
'submission_id': data.reportId ?? baseFileName, // This is the timestamp ID
|
||||||
'module': 'marine',
|
'module': 'marine',
|
||||||
'type': 'Investigative',
|
'type': 'Investigative',
|
||||||
'status': status,
|
'status': status,
|
||||||
'message': message,
|
'message': message,
|
||||||
'report_id': data.reportId,
|
'report_id': apiRecordId, // This is the server DB ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
'created_at': DateTime.now().toIso8601String(),
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
'form_data': jsonEncode(logMapData), // Log comprehensive map
|
'form_data': jsonEncode(logMapData), // Log comprehensive map
|
||||||
'image_data': jsonEncode(finalImageFiles.values.map((f) => f.path).toList()),
|
'image_data': jsonEncode(finalImageFiles.values.map((f) => f.path).toList()),
|
||||||
|
|||||||
@ -37,6 +37,22 @@ class MarineTarballSamplingService {
|
|||||||
|
|
||||||
MarineTarballSamplingService(this._telegramService);
|
MarineTarballSamplingService(this._telegramService);
|
||||||
|
|
||||||
|
// --- START: NEW HELPER METHOD ---
|
||||||
|
/// Generates a unique timestamp ID from the sampling date and time.
|
||||||
|
String _generateTimestampId(String? date, String? time) {
|
||||||
|
final String dateTimeString = "${date ?? ''} ${time ?? ''}";
|
||||||
|
try {
|
||||||
|
// Time format from model is HH:mm
|
||||||
|
final DateTime samplingDateTime = DateFormat('yyyy-MM-dd HH:mm').parse(dateTimeString);
|
||||||
|
return samplingDateTime.millisecondsSinceEpoch.toString();
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback: if parsing fails, use the current time in milliseconds
|
||||||
|
debugPrint("Could not parse '$dateTimeString' for timestamp ID, using current time. Error: $e");
|
||||||
|
return DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END: NEW HELPER METHOD ---
|
||||||
|
|
||||||
Future<Map<String, dynamic>> submitTarballSample({
|
Future<Map<String, dynamic>> submitTarballSample({
|
||||||
required TarballSamplingData data,
|
required TarballSamplingData data,
|
||||||
required List<Map<String, dynamic>>? appSettings,
|
required List<Map<String, dynamic>>? appSettings,
|
||||||
@ -46,6 +62,13 @@ class MarineTarballSamplingService {
|
|||||||
String? logDirectory, // Added for retry consistency
|
String? logDirectory, // Added for retry consistency
|
||||||
}) async {
|
}) async {
|
||||||
const String moduleName = 'marine_tarball';
|
const String moduleName = 'marine_tarball';
|
||||||
|
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// Generate the unique timestamp ID and assign it immediately.
|
||||||
|
final String timestampId = _generateTimestampId(data.samplingDate, data.samplingTime);
|
||||||
|
data.reportId = timestampId; // This is the primary ID now.
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
// --- START FIX: Handle nullable context ---
|
// --- START FIX: Handle nullable context ---
|
||||||
final authProvider = context != null ? Provider.of<AuthProvider>(context, listen: false) : null;
|
final authProvider = context != null ? Provider.of<AuthProvider>(context, listen: false) : null;
|
||||||
// Need a fallback mechanism if context is null (e.g., during retry)
|
// Need a fallback mechanism if context is null (e.g., during retry)
|
||||||
@ -118,6 +141,11 @@ class MarineTarballSamplingService {
|
|||||||
String finalStatus = '';
|
String finalStatus = '';
|
||||||
bool isSessionKnownToBeExpired = false;
|
bool isSessionKnownToBeExpired = false;
|
||||||
|
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
String? apiRecordId; // Will hold the DB ID (e.g., 102) from the server
|
||||||
|
// data.reportId already contains the timestamp ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Submit Form Data
|
// 1. Submit Form Data
|
||||||
apiDataResult = await _submissionApiService.submitPost(
|
apiDataResult = await _submissionApiService.submitPost(
|
||||||
@ -128,15 +156,20 @@ class MarineTarballSamplingService {
|
|||||||
|
|
||||||
if (apiDataResult['success'] == true) {
|
if (apiDataResult['success'] == true) {
|
||||||
anyApiSuccess = true;
|
anyApiSuccess = true;
|
||||||
data.reportId = apiDataResult['data']?['autoid']?.toString(); // Correct ID key
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// Store the server's database ID in a separate variable.
|
||||||
|
apiRecordId = apiDataResult['data']?['autoid']?.toString(); // Correct ID key
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
if (data.reportId != null) {
|
if (apiRecordId != null) {
|
||||||
if (finalImageFiles.isNotEmpty) {
|
if (finalImageFiles.isNotEmpty) {
|
||||||
// 2. Submit Images
|
// 2. Submit Images
|
||||||
apiImageResult = await _submissionApiService.submitMultipart(
|
apiImageResult = await _submissionApiService.submitMultipart(
|
||||||
moduleName: moduleName,
|
moduleName: moduleName,
|
||||||
endpoint: 'marine/tarball/images', // Correct endpoint
|
endpoint: 'marine/tarball/images', // Correct endpoint
|
||||||
fields: {'autoid': data.reportId!}, // Correct field key
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
fields: {'autoid': apiRecordId}, // Correct field key
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
files: finalImageFiles,
|
files: finalImageFiles,
|
||||||
);
|
);
|
||||||
if (apiImageResult['success'] != true) {
|
if (apiImageResult['success'] != true) {
|
||||||
@ -146,7 +179,9 @@ class MarineTarballSamplingService {
|
|||||||
// If data succeeded but no images, API part is still successful
|
// If data succeeded but no images, API part is still successful
|
||||||
} else {
|
} else {
|
||||||
anyApiSuccess = false;
|
anyApiSuccess = false;
|
||||||
apiDataResult['message'] = 'API Error: Submission succeeded but did not return a record ID.';
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
apiDataResult['message'] = 'API Error: Submission succeeded but did not return a server record ID.';
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If apiDataResult['success'] is false, SubmissionApiService queued it.
|
// If apiDataResult['success'] is false, SubmissionApiService queued it.
|
||||||
@ -158,10 +193,12 @@ class MarineTarballSamplingService {
|
|||||||
apiDataResult = {'success': false, 'message': 'Session expired. API submission queued.'};
|
apiDataResult = {'success': false, 'message': 'Session expired. API submission queued.'};
|
||||||
// Manually queue API calls
|
// Manually queue API calls
|
||||||
await _retryService.addApiToQueue(endpoint: 'marine/tarball/sample', method: 'POST', body: data.toFormData());
|
await _retryService.addApiToQueue(endpoint: 'marine/tarball/sample', method: 'POST', body: data.toFormData());
|
||||||
if (finalImageFiles.isNotEmpty && data.reportId != null) {
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
if (finalImageFiles.isNotEmpty && apiRecordId != null) {
|
||||||
// Queue images if data might have partially succeeded
|
// Queue images if data might have partially succeeded
|
||||||
await _retryService.addApiToQueue(endpoint: 'marine/tarball/images', method: 'POST_MULTIPART', fields: {'autoid': data.reportId!}, files: finalImageFiles);
|
await _retryService.addApiToQueue(endpoint: 'marine/tarball/images', method: 'POST_MULTIPART', fields: {'autoid': apiRecordId}, files: finalImageFiles);
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Submit FTP Files
|
// 3. Submit FTP Files
|
||||||
@ -170,7 +207,9 @@ class MarineTarballSamplingService {
|
|||||||
|
|
||||||
if (isSessionKnownToBeExpired) {
|
if (isSessionKnownToBeExpired) {
|
||||||
debugPrint("Skipping FTP attempt due to known expired session. Manually queuing FTP tasks.");
|
debugPrint("Skipping FTP attempt due to known expired session. Manually queuing FTP tasks.");
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
final baseFileNameForQueue = _generateBaseFileName(data); // Use helper
|
final baseFileNameForQueue = _generateBaseFileName(data); // Use helper
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
// --- START FIX: Add ftpConfigId when queuing ---
|
// --- START FIX: Add ftpConfigId when queuing ---
|
||||||
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
||||||
@ -259,6 +298,9 @@ class MarineTarballSamplingService {
|
|||||||
ftpStatuses: ftpResults['statuses'],
|
ftpStatuses: ftpResults['statuses'],
|
||||||
serverName: serverName,
|
serverName: serverName,
|
||||||
finalImageFiles: finalImageFiles,
|
finalImageFiles: finalImageFiles,
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
apiRecordId: apiRecordId, // Pass the server DB ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
logDirectory: logDirectory, // Pass logDirectory for potential update
|
logDirectory: logDirectory, // Pass logDirectory for potential update
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -267,7 +309,7 @@ class MarineTarballSamplingService {
|
|||||||
_handleTarballSuccessAlert(data, appSettings, isDataOnly: finalImageFiles.isEmpty, isSessionExpired: isSessionKnownToBeExpired);
|
_handleTarballSuccessAlert(data, appSettings, isDataOnly: finalImageFiles.isEmpty, isSessionExpired: isSessionKnownToBeExpired);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {'success': overallSuccess, 'message': finalMessage, 'reportId': data.reportId};
|
return {'success': overallSuccess, 'message': finalMessage, 'reportId': data.reportId}; // Return timestamp ID
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> _performOfflineQueuing({
|
Future<Map<String, dynamic>> _performOfflineQueuing({
|
||||||
@ -296,8 +338,10 @@ class MarineTarballSamplingService {
|
|||||||
|
|
||||||
if (savedLogPath == null) {
|
if (savedLogPath == null) {
|
||||||
const message = "Failed to save submission to local device storage.";
|
const message = "Failed to save submission to local device storage.";
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
// Log failure state if saving fails
|
// Log failure state if saving fails
|
||||||
await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}, logDirectory: logDirectory);
|
await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}, apiRecordId: null, logDirectory: logDirectory);
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
return {'success': false, 'message': message};
|
return {'success': false, 'message': message};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,17 +357,25 @@ class MarineTarballSamplingService {
|
|||||||
|
|
||||||
const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
|
const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
|
||||||
// Log final queued state to central DB
|
// Log final queued state to central DB
|
||||||
// await _logAndSave(data: data, status: 'Queued', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}, logDirectory: savedLogPath);
|
// await _logAndSave(data: data, status: 'Queued', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}, apiRecordId: null, logDirectory: savedLogPath);
|
||||||
|
|
||||||
return {'success': true, 'message': successMessage, 'reportId': null}; // No report ID yet
|
return {'success': true, 'message': successMessage, 'reportId': data.reportId}; // Return timestamp ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: MODIFIED _generateBaseFileName ---
|
||||||
/// Helper to generate the base filename for ZIP files.
|
/// Helper to generate the base filename for ZIP files.
|
||||||
String _generateBaseFileName(TarballSamplingData data) {
|
String _generateBaseFileName(TarballSamplingData data) {
|
||||||
final stationCode = data.selectedStation?['tbl_station_code'] ?? 'NA';
|
final stationCode = data.selectedStation?['tbl_station_code'] ?? 'NA';
|
||||||
final fileTimestamp = "${data.samplingDate}_${data.samplingTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
|
||||||
return '${stationCode}_$fileTimestamp';
|
// We now always use data.reportId, which we set as the timestamp.
|
||||||
|
if (data.reportId == null || data.reportId!.isEmpty) {
|
||||||
|
// This is a safety fallback, but should not happen if submitData is used.
|
||||||
|
debugPrint("Warning: reportId is null in _generateBaseFileName. Using current timestamp.");
|
||||||
|
return '${stationCode}_${DateTime.now().millisecondsSinceEpoch.toString()}';
|
||||||
|
}
|
||||||
|
return '${stationCode}_${data.reportId}';
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED _generateBaseFileName ---
|
||||||
|
|
||||||
/// Generates data and image ZIP files and uploads them using SubmissionFtpService.
|
/// Generates data and image ZIP files and uploads them using SubmissionFtpService.
|
||||||
Future<Map<String, dynamic>> _generateAndUploadFtpFiles(TarballSamplingData data, Map<String, File> imageFiles, String serverName, String moduleName) async {
|
Future<Map<String, dynamic>> _generateAndUploadFtpFiles(TarballSamplingData data, Map<String, File> imageFiles, String serverName, String moduleName) async {
|
||||||
@ -334,7 +386,9 @@ class MarineTarballSamplingService {
|
|||||||
module: 'marine',
|
module: 'marine',
|
||||||
subModule: 'marine_tarball_sampling', // Correct sub-module
|
subModule: 'marine_tarball_sampling', // Correct sub-module
|
||||||
);
|
);
|
||||||
final folderName = data.reportId ?? baseFileName;
|
// --- 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, folderName)) : null;
|
||||||
|
|
||||||
if (localSubmissionDir != null && !await localSubmissionDir.exists()) {
|
if (localSubmissionDir != null && !await localSubmissionDir.exists()) {
|
||||||
@ -393,6 +447,9 @@ class MarineTarballSamplingService {
|
|||||||
required List<Map<String, dynamic>> ftpStatuses,
|
required List<Map<String, dynamic>> ftpStatuses,
|
||||||
required String serverName,
|
required String serverName,
|
||||||
required Map<String, File> finalImageFiles,
|
required Map<String, File> finalImageFiles,
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
String? apiRecordId, // This is the server DB ID (e.g., 102)
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
String? logDirectory, // Added for potential update
|
String? logDirectory, // Added for potential update
|
||||||
}) async {
|
}) async {
|
||||||
data.submissionStatus = status;
|
data.submissionStatus = status;
|
||||||
@ -408,7 +465,10 @@ class MarineTarballSamplingService {
|
|||||||
// Add submission metadata
|
// Add submission metadata
|
||||||
logMapData['submissionStatus'] = status;
|
logMapData['submissionStatus'] = status;
|
||||||
logMapData['submissionMessage'] = message;
|
logMapData['submissionMessage'] = message;
|
||||||
logMapData['reportId'] = data.reportId;
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// data.reportId (the timestamp) is already in the map from toDbJson()
|
||||||
|
logMapData['apiRecordId'] = apiRecordId; // Add the server DB ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
logMapData['serverConfigName'] = serverName;
|
logMapData['serverConfigName'] = serverName;
|
||||||
logMapData['api_status'] = jsonEncode(apiResults.where((r) => r.isNotEmpty).toList());
|
logMapData['api_status'] = jsonEncode(apiResults.where((r) => r.isNotEmpty).toList());
|
||||||
logMapData['ftp_status'] = jsonEncode(ftpStatuses);
|
logMapData['ftp_status'] = jsonEncode(ftpStatuses);
|
||||||
@ -423,12 +483,14 @@ class MarineTarballSamplingService {
|
|||||||
|
|
||||||
// Save a record to the central SQLite submission log table
|
// Save a record to the central SQLite submission log table
|
||||||
final logData = {
|
final logData = {
|
||||||
'submission_id': data.reportId ?? baseFileName, // Use helper result
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
'submission_id': data.reportId ?? baseFileName, // This is the timestamp ID
|
||||||
'module': 'marine', // Correct module
|
'module': 'marine', // Correct module
|
||||||
'type': 'Tarball', // Correct type
|
'type': 'Tarball', // Correct type
|
||||||
'status': status,
|
'status': status,
|
||||||
'message': message,
|
'message': message,
|
||||||
'report_id': data.reportId,
|
'report_id': apiRecordId, // This is the server DB ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
'created_at': DateTime.now().toIso8601String(),
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
'form_data': jsonEncode(logMapData), // Log the comprehensive map with paths
|
'form_data': jsonEncode(logMapData), // Log the comprehensive map with paths
|
||||||
'image_data': jsonEncode(finalImageFiles.values.map((f) => f.path).toList()),
|
'image_data': jsonEncode(finalImageFiles.values.map((f) => f.path).toList()),
|
||||||
|
|||||||
@ -163,6 +163,22 @@ class RiverInSituSamplingService {
|
|||||||
_serialManager.dispose();
|
_serialManager.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: NEW HELPER METHOD ---
|
||||||
|
/// Generates a unique timestamp ID from the sampling date and time.
|
||||||
|
String _generateTimestampId(String? date, String? time) {
|
||||||
|
final String dateTimeString = "${date ?? ''} ${time ?? ''}";
|
||||||
|
try {
|
||||||
|
// Time format from model is HH:mm
|
||||||
|
final DateTime samplingDateTime = DateFormat('yyyy-MM-dd HH:mm').parse(dateTimeString);
|
||||||
|
return samplingDateTime.millisecondsSinceEpoch.toString();
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback: if parsing fails, use the current time in milliseconds
|
||||||
|
debugPrint("Could not parse '$dateTimeString' for timestamp ID, using current time. Error: $e");
|
||||||
|
return DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END: NEW HELPER METHOD ---
|
||||||
|
|
||||||
Future<Map<String, dynamic>> submitData({
|
Future<Map<String, dynamic>> submitData({
|
||||||
required RiverInSituSamplingData data,
|
required RiverInSituSamplingData data,
|
||||||
required List<Map<String, dynamic>>? appSettings,
|
required List<Map<String, dynamic>>? appSettings,
|
||||||
@ -171,6 +187,13 @@ class RiverInSituSamplingService {
|
|||||||
}) async {
|
}) async {
|
||||||
const String moduleName = 'river_in_situ';
|
const String moduleName = 'river_in_situ';
|
||||||
|
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// Generate the unique timestamp ID and assign it immediately.
|
||||||
|
// This ID will be used for filenames and as the primary submission ID.
|
||||||
|
final String timestampId = _generateTimestampId(data.samplingDate, data.samplingTime);
|
||||||
|
data.reportId = timestampId; // This is the primary ID now.
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
final connectivityResult = await Connectivity().checkConnectivity();
|
final connectivityResult = await Connectivity().checkConnectivity();
|
||||||
bool isOnline = !connectivityResult.contains(ConnectivityResult.none);
|
bool isOnline = !connectivityResult.contains(ConnectivityResult.none);
|
||||||
bool isOfflineSession = authProvider.isLoggedIn && (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false);
|
bool isOfflineSession = authProvider.isLoggedIn && (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false);
|
||||||
@ -232,6 +255,11 @@ class RiverInSituSamplingService {
|
|||||||
String finalStatus = '';
|
String finalStatus = '';
|
||||||
bool isSessionKnownToBeExpired = false;
|
bool isSessionKnownToBeExpired = false;
|
||||||
|
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
String? apiRecordId; // Will hold the DB ID (e.g., 102) from the server
|
||||||
|
// data.reportId already contains the timestamp ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Submit Form Data
|
// 1. Submit Form Data
|
||||||
apiDataResult = await _submissionApiService.submitPost(
|
apiDataResult = await _submissionApiService.submitPost(
|
||||||
@ -242,15 +270,21 @@ class RiverInSituSamplingService {
|
|||||||
|
|
||||||
if (apiDataResult['success'] == true) {
|
if (apiDataResult['success'] == true) {
|
||||||
anyApiSuccess = true;
|
anyApiSuccess = true;
|
||||||
data.reportId = apiDataResult['data']?['r_man_id']?.toString(); // Correct ID key
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// Store the server's database ID in a separate variable.
|
||||||
|
// data.reportId (the timestamp) REMAINS UNCHANGED.
|
||||||
|
apiRecordId = apiDataResult['data']?['r_man_id']?.toString(); // Correct ID key
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
if (data.reportId != null) {
|
if (apiRecordId != null) { // Check if server returned an ID
|
||||||
if (finalImageFiles.isNotEmpty) {
|
if (finalImageFiles.isNotEmpty) {
|
||||||
// 2. Submit Images
|
// 2. Submit Images
|
||||||
apiImageResult = await _submissionApiService.submitMultipart(
|
apiImageResult = await _submissionApiService.submitMultipart(
|
||||||
moduleName: moduleName,
|
moduleName: moduleName,
|
||||||
endpoint: 'river/manual/images', // Correct endpoint
|
endpoint: 'river/manual/images', // Correct endpoint
|
||||||
fields: {'r_man_id': data.reportId!}, // Correct field key
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
fields: {'r_man_id': apiRecordId}, // Use server's ID for relation
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
files: finalImageFiles,
|
files: finalImageFiles,
|
||||||
);
|
);
|
||||||
if (apiImageResult['success'] != true) {
|
if (apiImageResult['success'] != true) {
|
||||||
@ -259,7 +293,9 @@ class RiverInSituSamplingService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
anyApiSuccess = false;
|
anyApiSuccess = false;
|
||||||
apiDataResult['message'] = 'API Error: Submission succeeded but did not return a record ID.';
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
apiDataResult['message'] = 'API Error: Submission succeeded but did not return a server record ID.';
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If apiDataResult['success'] is false, SubmissionApiService queued it.
|
// If apiDataResult['success'] is false, SubmissionApiService queued it.
|
||||||
@ -271,9 +307,11 @@ class RiverInSituSamplingService {
|
|||||||
apiDataResult = {'success': false, 'message': 'Session expired and re-login failed. API submission queued.'};
|
apiDataResult = {'success': false, 'message': 'Session expired and re-login failed. API submission queued.'};
|
||||||
// Manually queue API calls
|
// Manually queue API calls
|
||||||
await _retryService.addApiToQueue(endpoint: 'river/manual/sample', method: 'POST', body: data.toApiFormData());
|
await _retryService.addApiToQueue(endpoint: 'river/manual/sample', method: 'POST', body: data.toApiFormData());
|
||||||
if (finalImageFiles.isNotEmpty && data.reportId != null) {
|
if (finalImageFiles.isNotEmpty && apiRecordId != null) {
|
||||||
// Also queue images if data call might have partially succeeded before expiry
|
// Also queue images if data call might have partially succeeded before expiry
|
||||||
await _retryService.addApiToQueue(endpoint: 'river/manual/images', method: 'POST_MULTIPART', fields: {'r_man_id': data.reportId!}, files: finalImageFiles);
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
await _retryService.addApiToQueue(endpoint: 'river/manual/images', method: 'POST_MULTIPART', fields: {'r_man_id': apiRecordId}, files: finalImageFiles);
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,7 +321,10 @@ class RiverInSituSamplingService {
|
|||||||
|
|
||||||
if (isSessionKnownToBeExpired) {
|
if (isSessionKnownToBeExpired) {
|
||||||
debugPrint("Skipping FTP attempt due to known expired session. Manually queuing FTP tasks.");
|
debugPrint("Skipping FTP attempt due to known expired session. Manually queuing FTP tasks.");
|
||||||
final baseFileNameForQueue = _generateBaseFileName(data); // Use helper
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// We can now safely call _generateBaseFileName, as data.reportId is the timestamp
|
||||||
|
final baseFileNameForQueue = _generateBaseFileName(data);
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
// --- START FIX: Add ftpConfigId when queuing ---
|
// --- START FIX: Add ftpConfigId when queuing ---
|
||||||
// Get all potential FTP configs
|
// Get all potential FTP configs
|
||||||
@ -371,6 +412,9 @@ class RiverInSituSamplingService {
|
|||||||
apiResults: [apiDataResult, apiImageResult],
|
apiResults: [apiDataResult, apiImageResult],
|
||||||
ftpStatuses: ftpResults['statuses'],
|
ftpStatuses: ftpResults['statuses'],
|
||||||
serverName: serverName,
|
serverName: serverName,
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
apiRecordId: apiRecordId, // Pass the server DB ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
logDirectory: logDirectory,
|
logDirectory: logDirectory,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -387,7 +431,9 @@ class RiverInSituSamplingService {
|
|||||||
'status': finalStatus,
|
'status': finalStatus,
|
||||||
'success': overallSuccess,
|
'success': overallSuccess,
|
||||||
'message': finalMessage,
|
'message': finalMessage,
|
||||||
'reportId': data.reportId
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
'reportId': data.reportId // This is now the timestamp ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,7 +463,9 @@ class RiverInSituSamplingService {
|
|||||||
|
|
||||||
if (savedLogPath == null) {
|
if (savedLogPath == null) {
|
||||||
const message = "Failed to save submission to local device storage.";
|
const message = "Failed to save submission to local device storage.";
|
||||||
await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, logDirectory: logDirectory);
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, apiRecordId: null, logDirectory: logDirectory);
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
return {'status': 'Error', 'success': false, 'message': message};
|
return {'status': 'Error', 'success': false, 'message': message};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,9 +480,9 @@ class RiverInSituSamplingService {
|
|||||||
|
|
||||||
const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
|
const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
|
||||||
// Log final queued state to central DB
|
// Log final queued state to central DB
|
||||||
// await _logAndSave(data: data, status: 'Queued', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, logDirectory: savedLogPath);
|
// await _logAndSave(data: data, status: 'Queued', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, apiRecordId: null, logDirectory: savedLogPath);
|
||||||
|
|
||||||
return {'status': 'Queued', 'success': true, 'message': successMessage, 'reportId': null};
|
return {'status': 'Queued', 'success': true, 'message': successMessage, 'reportId': data.reportId}; // Return timestamp ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: MODIFIED _generateBaseFileName ---
|
// --- START: MODIFIED _generateBaseFileName ---
|
||||||
@ -442,14 +490,13 @@ class RiverInSituSamplingService {
|
|||||||
String _generateBaseFileName(RiverInSituSamplingData data) {
|
String _generateBaseFileName(RiverInSituSamplingData data) {
|
||||||
final stationCode = data.selectedStation?['sampling_station_code'] ?? 'UNKNOWN';
|
final stationCode = data.selectedStation?['sampling_station_code'] ?? 'UNKNOWN';
|
||||||
|
|
||||||
// Check if reportId (timestamp) is available.
|
// We now always use data.reportId, which we set as the timestamp.
|
||||||
if (data.reportId != null && data.reportId!.isNotEmpty) {
|
if (data.reportId == null || data.reportId!.isEmpty) {
|
||||||
return '${stationCode}_${data.reportId}';
|
// This is a safety fallback, but should not happen if submitData is used.
|
||||||
} else {
|
debugPrint("Warning: reportId is null in _generateBaseFileName. Using current timestamp.");
|
||||||
// Fallback to old method if reportId is not available (e.g., offline queue)
|
return '${stationCode}_${DateTime.now().millisecondsSinceEpoch.toString()}';
|
||||||
final fileTimestamp = "${data.samplingDate}_${data.samplingTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
|
||||||
return "${stationCode}_$fileTimestamp";
|
|
||||||
}
|
}
|
||||||
|
return '${stationCode}_${data.reportId}';
|
||||||
}
|
}
|
||||||
// --- END: MODIFIED _generateBaseFileName ---
|
// --- END: MODIFIED _generateBaseFileName ---
|
||||||
|
|
||||||
@ -514,18 +561,25 @@ class RiverInSituSamplingService {
|
|||||||
required List<Map<String, dynamic>> apiResults,
|
required List<Map<String, dynamic>> apiResults,
|
||||||
required List<Map<String, dynamic>> ftpStatuses,
|
required List<Map<String, dynamic>> ftpStatuses,
|
||||||
required String serverName,
|
required String serverName,
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
String? apiRecordId, // This is the server DB ID (e.g., 102)
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
String? logDirectory,
|
String? logDirectory,
|
||||||
}) async {
|
}) async {
|
||||||
data.submissionStatus = status;
|
data.submissionStatus = status;
|
||||||
data.submissionMessage = message;
|
data.submissionMessage = message;
|
||||||
final baseFileName = _generateBaseFileName(data); // Use helper
|
final baseFileName = _generateBaseFileName(data); // This now uses the timestamp ID
|
||||||
|
|
||||||
// Prepare log data map using toMap()
|
// Prepare log data map using toMap()
|
||||||
final Map<String, dynamic> logMapData = data.toMap();
|
final Map<String, dynamic> logMapData = data.toMap();
|
||||||
// Add submission metadata
|
// Add submission metadata
|
||||||
logMapData['submissionStatus'] = status;
|
logMapData['submissionStatus'] = status;
|
||||||
logMapData['submissionMessage'] = message;
|
logMapData['submissionMessage'] = message;
|
||||||
logMapData['reportId'] = data.reportId;
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// data.reportId (which is the timestamp) is already in the map from toMap()
|
||||||
|
// We add the server's DB ID separately for the log file
|
||||||
|
logMapData['apiRecordId'] = apiRecordId; // (e.g., 102)
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
logMapData['serverConfigName'] = serverName;
|
logMapData['serverConfigName'] = serverName;
|
||||||
logMapData['api_status'] = jsonEncode(apiResults.where((r) => r.isNotEmpty).toList());
|
logMapData['api_status'] = jsonEncode(apiResults.where((r) => r.isNotEmpty).toList());
|
||||||
logMapData['ftp_status'] = jsonEncode(ftpStatuses);
|
logMapData['ftp_status'] = jsonEncode(ftpStatuses);
|
||||||
@ -542,12 +596,14 @@ class RiverInSituSamplingService {
|
|||||||
// Save to central DB log
|
// Save to central DB log
|
||||||
final imagePaths = data.toApiImageFiles().values.whereType<File>().map((f) => f.path).toList();
|
final imagePaths = data.toApiImageFiles().values.whereType<File>().map((f) => f.path).toList();
|
||||||
final centralLogData = {
|
final centralLogData = {
|
||||||
'submission_id': data.reportId ?? baseFileName, // Use helper result
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
'submission_id': data.reportId ?? baseFileName, // This is the timestamp ID
|
||||||
'module': 'river',
|
'module': 'river',
|
||||||
'type': data.samplingType ?? 'In-Situ', // Correct type
|
'type': data.samplingType ?? 'In-Situ', // Correct type
|
||||||
'status': status,
|
'status': status,
|
||||||
'message': message,
|
'message': message,
|
||||||
'report_id': data.reportId,
|
'report_id': apiRecordId, // This is the server DB ID (e.g., 102)
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
'created_at': DateTime.now().toIso8601String(),
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
'form_data': jsonEncode(logMapData), // Log the comprehensive map
|
'form_data': jsonEncode(logMapData), // Log the comprehensive map
|
||||||
'image_data': jsonEncode(imagePaths),
|
'image_data': jsonEncode(imagePaths),
|
||||||
|
|||||||
@ -176,6 +176,22 @@ class RiverInvestigativeSamplingService { // Renamed class
|
|||||||
_serialManager.dispose();
|
_serialManager.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: NEW HELPER METHOD ---
|
||||||
|
/// Generates a unique timestamp ID from the sampling date and time.
|
||||||
|
String _generateTimestampId(String? date, String? time) {
|
||||||
|
final String dateTimeString = "${date ?? ''} ${time ?? ''}";
|
||||||
|
try {
|
||||||
|
// Time format from model is HH:mm
|
||||||
|
final DateTime samplingDateTime = DateFormat('yyyy-MM-dd HH:mm').parse(dateTimeString);
|
||||||
|
return samplingDateTime.millisecondsSinceEpoch.toString();
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback: if parsing fails, use the current time in milliseconds
|
||||||
|
debugPrint("Could not parse '$dateTimeString' for timestamp ID, using current time. Error: $e");
|
||||||
|
return DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END: NEW HELPER METHOD ---
|
||||||
|
|
||||||
// Adapted Submission Logic for Investigative
|
// Adapted Submission Logic for Investigative
|
||||||
Future<Map<String, dynamic>> submitData({
|
Future<Map<String, dynamic>> submitData({
|
||||||
required RiverInvesManualSamplingData data, // Updated model type
|
required RiverInvesManualSamplingData data, // Updated model type
|
||||||
@ -186,6 +202,12 @@ class RiverInvestigativeSamplingService { // Renamed class
|
|||||||
// *** MODIFIED: Module name changed ***
|
// *** MODIFIED: Module name changed ***
|
||||||
const String moduleName = 'river_investigative';
|
const String moduleName = 'river_investigative';
|
||||||
|
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// Generate the unique timestamp ID and assign it immediately.
|
||||||
|
final String timestampId = _generateTimestampId(data.samplingDate, data.samplingTime);
|
||||||
|
data.reportId = timestampId; // This is the primary ID now.
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
final connectivityResult = await Connectivity().checkConnectivity();
|
final connectivityResult = await Connectivity().checkConnectivity();
|
||||||
bool isOnline = !connectivityResult.contains(ConnectivityResult.none);
|
bool isOnline = !connectivityResult.contains(ConnectivityResult.none);
|
||||||
bool isOfflineSession = authProvider.isLoggedIn && (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false);
|
bool isOfflineSession = authProvider.isLoggedIn && (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false);
|
||||||
@ -246,6 +268,11 @@ class RiverInvestigativeSamplingService { // Renamed class
|
|||||||
String finalStatus = '';
|
String finalStatus = '';
|
||||||
bool isSessionKnownToBeExpired = false;
|
bool isSessionKnownToBeExpired = false;
|
||||||
|
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
String? apiRecordId; // Will hold the DB ID (e.g., 102) from the server
|
||||||
|
// data.reportId already contains the timestamp ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Submit Form Data (using Investigative endpoint and data)
|
// 1. Submit Form Data (using Investigative endpoint and data)
|
||||||
apiDataResult = await _submissionApiService.submitPost(
|
apiDataResult = await _submissionApiService.submitPost(
|
||||||
@ -257,18 +284,22 @@ class RiverInvestigativeSamplingService { // Renamed class
|
|||||||
|
|
||||||
if (apiDataResult['success'] == true) {
|
if (apiDataResult['success'] == true) {
|
||||||
anyApiSuccess = true;
|
anyApiSuccess = true;
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
// *** MODIFIED: Extract report ID using assumed key ***
|
// *** MODIFIED: Extract report ID using assumed key ***
|
||||||
data.reportId = apiDataResult['data']?['r_inv_id']?.toString(); // Assumed key for investigative ID
|
apiRecordId = apiDataResult['data']?['r_inv_id']?.toString(); // Assumed key for investigative ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
if (data.reportId != null) {
|
if (apiRecordId != null) {
|
||||||
if (finalImageFiles.isNotEmpty) {
|
if (finalImageFiles.isNotEmpty) {
|
||||||
// 2. Submit Images (using Investigative endpoint)
|
// 2. Submit Images (using Investigative endpoint)
|
||||||
apiImageResult = await _submissionApiService.submitMultipart(
|
apiImageResult = await _submissionApiService.submitMultipart(
|
||||||
moduleName: moduleName, // 'river_investigative'
|
moduleName: moduleName, // 'river_investigative'
|
||||||
// *** MODIFIED: API Endpoint ***
|
// *** MODIFIED: API Endpoint ***
|
||||||
endpoint: 'river/investigative/images', // Assumed endpoint for investigative images
|
endpoint: 'river/investigative/images', // Assumed endpoint for investigative images
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
// *** MODIFIED: Field key for ID ***
|
// *** MODIFIED: Field key for ID ***
|
||||||
fields: {'r_inv_id': data.reportId!}, // Use assumed investigative ID key
|
fields: {'r_inv_id': apiRecordId}, // Use assumed investigative ID key
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
files: finalImageFiles,
|
files: finalImageFiles,
|
||||||
);
|
);
|
||||||
if (apiImageResult['success'] != true) {
|
if (apiImageResult['success'] != true) {
|
||||||
@ -281,7 +312,9 @@ class RiverInvestigativeSamplingService { // Renamed class
|
|||||||
// API succeeded but didn't return an ID - treat as failure
|
// API succeeded but didn't return an ID - treat as failure
|
||||||
anyApiSuccess = false;
|
anyApiSuccess = false;
|
||||||
apiDataResult['success'] = false; // Mark as failed
|
apiDataResult['success'] = false; // Mark as failed
|
||||||
apiDataResult['message'] = 'API Error: Submission succeeded but did not return a record ID.';
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
apiDataResult['message'] = 'API Error: Submission succeeded but did not return a server record ID.';
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If apiDataResult['success'] is false initially, SubmissionApiService queued it.
|
// If apiDataResult['success'] is false initially, SubmissionApiService queued it.
|
||||||
@ -294,10 +327,12 @@ class RiverInvestigativeSamplingService { // Renamed class
|
|||||||
// Manually queue API calls
|
// Manually queue API calls
|
||||||
// *** MODIFIED: Use Investigative endpoints for queueing ***
|
// *** MODIFIED: Use Investigative endpoints for queueing ***
|
||||||
await _retryService.addApiToQueue(endpoint: 'river/investigative/sample', method: 'POST', body: data.toApiFormData());
|
await _retryService.addApiToQueue(endpoint: 'river/investigative/sample', method: 'POST', body: data.toApiFormData());
|
||||||
if (finalImageFiles.isNotEmpty && data.reportId != null) {
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
if (finalImageFiles.isNotEmpty && apiRecordId != null) {
|
||||||
// Queue images only if we might have gotten an ID before expiry
|
// Queue images only if we might have gotten an ID before expiry
|
||||||
await _retryService.addApiToQueue(endpoint: 'river/investigative/images', method: 'POST_MULTIPART', fields: {'r_inv_id': data.reportId!}, files: finalImageFiles);
|
await _retryService.addApiToQueue(endpoint: 'river/investigative/images', method: 'POST_MULTIPART', fields: {'r_inv_id': apiRecordId}, files: finalImageFiles);
|
||||||
} else if (finalImageFiles.isNotEmpty && data.reportId == null) {
|
} else if (finalImageFiles.isNotEmpty && apiRecordId == null) {
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
// If data call failed before getting ID, queue images without ID - might need manual linking later or separate retry logic
|
// If data call failed before getting ID, queue images without ID - might need manual linking later or separate retry logic
|
||||||
debugPrint("Queueing investigative images without report ID due to session expiry during data submission.");
|
debugPrint("Queueing investigative images without report ID due to session expiry during data submission.");
|
||||||
// How to handle this depends on backend capabilities or manual intervention needs.
|
// How to handle this depends on backend capabilities or manual intervention needs.
|
||||||
@ -312,7 +347,9 @@ class RiverInvestigativeSamplingService { // Renamed class
|
|||||||
|
|
||||||
if (isSessionKnownToBeExpired) {
|
if (isSessionKnownToBeExpired) {
|
||||||
debugPrint("Skipping FTP attempt for River Investigative due to known expired session. Manually queuing FTP tasks."); // Log context update
|
debugPrint("Skipping FTP attempt for River Investigative due to known expired session. Manually queuing FTP tasks."); // Log context update
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
final baseFileNameForQueue = _generateBaseFileName(data); // Use helper
|
final baseFileNameForQueue = _generateBaseFileName(data); // Use helper
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
// --- START FIX: Add ftpConfigId when queuing --- (Copied from In-Situ, ensure DB structure matches)
|
// --- START FIX: Add ftpConfigId when queuing --- (Copied from In-Situ, ensure DB structure matches)
|
||||||
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
||||||
@ -402,6 +439,9 @@ class RiverInvestigativeSamplingService { // Renamed class
|
|||||||
apiResults: [apiDataResult, apiImageResult].where((r) => r.isNotEmpty).toList(), // Filter out empty results
|
apiResults: [apiDataResult, apiImageResult].where((r) => r.isNotEmpty).toList(), // Filter out empty results
|
||||||
ftpStatuses: ftpResults['statuses'] ?? [],
|
ftpStatuses: ftpResults['statuses'] ?? [],
|
||||||
serverName: serverName,
|
serverName: serverName,
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
apiRecordId: apiRecordId, // Pass the server ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
logDirectory: logDirectory,
|
logDirectory: logDirectory,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -415,7 +455,9 @@ class RiverInvestigativeSamplingService { // Renamed class
|
|||||||
'status': finalStatus,
|
'status': finalStatus,
|
||||||
'success': overallSuccess, // Reflects if *any* part succeeded now
|
'success': overallSuccess, // Reflects if *any* part succeeded now
|
||||||
'message': finalMessage,
|
'message': finalMessage,
|
||||||
'reportId': data.reportId // May be null if API failed
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
'reportId': data.reportId // This is now the timestamp ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -449,7 +491,9 @@ class RiverInvestigativeSamplingService { // Renamed class
|
|||||||
// If saving the log itself failed
|
// If saving the log itself failed
|
||||||
const message = "Failed to save River Investigative submission to local device storage."; // Log context update
|
const message = "Failed to save River Investigative submission to local device storage."; // Log context update
|
||||||
// Log failure to central DB log if possible
|
// Log failure to central DB log if possible
|
||||||
await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, logDirectory: logDirectory);
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, apiRecordId: null, logDirectory: logDirectory);
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
return {'status': 'Error', 'success': false, 'message': message};
|
return {'status': 'Error', 'success': false, 'message': message};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -469,174 +513,14 @@ class RiverInvestigativeSamplingService { // Renamed class
|
|||||||
data.submissionStatus = 'L1'; // Final queued status
|
data.submissionStatus = 'L1'; // Final queued status
|
||||||
data.submissionMessage = successMessage;
|
data.submissionMessage = successMessage;
|
||||||
// Log final queued state to central DB log
|
// Log final queued state to central DB log
|
||||||
await _logAndSave(data: data, status: 'L1', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, logDirectory: savedLogPath); // Ensure log reflects final state
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
await _logAndSave(data: data, status: 'L1', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, apiRecordId: null, logDirectory: savedLogPath); // Ensure log reflects final state
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
return {'status': 'Queued', 'success': true, 'message': successMessage, 'reportId': null};
|
return {'status': 'Queued', 'success': true, 'message': successMessage, 'reportId': data.reportId}; // Return timestamp ID
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper to generate the base filename for ZIP files (Investigative).
|
|
||||||
String _generateBaseFileName(RiverInvesManualSamplingData data) { // Updated model type
|
|
||||||
// Use the determined station code helper
|
|
||||||
final stationCode = data.getDeterminedStationCode() ?? 'UNKNOWN';
|
|
||||||
final fileTimestamp = "${data.samplingDate}_${data.samplingTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
|
||||||
return "${stationCode}_$fileTimestamp"; // Consistent format
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
final baseFileName = _generateBaseFileName(data); // Use helper
|
|
||||||
|
|
||||||
// *** MODIFIED: Use correct base dir getter ***
|
|
||||||
final Directory? logDirectory = await _localStorageService.getRiverInvestigativeBaseDir(serverName: serverName); // NEW GETTER
|
|
||||||
|
|
||||||
// Determine the specific folder for this submission log within the base directory
|
|
||||||
final folderName = data.reportId ?? baseFileName; // Use report ID if available, else generated name
|
|
||||||
final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, folderName)) : 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)
|
|
||||||
final dataZip = await _zippingService.createDataZip(
|
|
||||||
jsonDataMap: {
|
|
||||||
// *** MODIFIED: Use Investigative model's JSON methods and filenames ***
|
|
||||||
'db.json': jsonEncode(data.toDbJson()), // Main data structure
|
|
||||||
'river_inves_basic_form.json': data.toBasicFormJson(),
|
|
||||||
'river_inves_reading.json': data.toReadingJson(),
|
|
||||||
'river_inves_manual_info.json': data.toManualInfoJson(),
|
|
||||||
},
|
|
||||||
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(
|
|
||||||
moduleName: moduleName, // 'river_investigative'
|
|
||||||
fileToUpload: dataZip,
|
|
||||||
remotePath: '/${p.basename(dataZip.path)}' // Standard remote path
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(),
|
|
||||||
baseFileName: baseFileName,
|
|
||||||
destinationDir: localSubmissionDir, // Save ZIP in the specific log folder
|
|
||||||
);
|
|
||||||
if (imageZip != null) {
|
|
||||||
ftpImageResult = await _submissionFtpService.submit(
|
|
||||||
moduleName: moduleName, // 'river_investigative'
|
|
||||||
fileToUpload: imageZip,
|
|
||||||
remotePath: '/${p.basename(imageZip.path)}' // Standard remote path
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Combine statuses from both uploads
|
|
||||||
return {
|
|
||||||
'statuses': <Map<String, dynamic>>[
|
|
||||||
...(ftpDataResult['statuses'] as List? ?? []), // Use null-aware spread
|
|
||||||
...(ftpImageResult['statuses'] as List? ?? []), // Use null-aware spread
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Saves or updates the local log file and saves a record to the central DB log (Investigative).
|
|
||||||
Future<void> _logAndSave({
|
|
||||||
required RiverInvesManualSamplingData data, // Updated model type
|
|
||||||
required String status,
|
|
||||||
required String message,
|
|
||||||
required List<Map<String, dynamic>> apiResults,
|
|
||||||
required List<Map<String, dynamic>> ftpStatuses,
|
|
||||||
required String serverName,
|
|
||||||
String? logDirectory, // Can be null initially, gets populated on first save
|
|
||||||
}) async {
|
|
||||||
data.submissionStatus = status;
|
|
||||||
data.submissionMessage = message;
|
|
||||||
final baseFileName = _generateBaseFileName(data); // Use helper for consistent naming
|
|
||||||
|
|
||||||
// Prepare log data map using toMap()
|
|
||||||
final Map<String, dynamic> logMapData = data.toMap();
|
|
||||||
// Add submission metadata that might not be in toMap() or needs overriding
|
|
||||||
logMapData['submissionStatus'] = status;
|
|
||||||
logMapData['submissionMessage'] = message;
|
|
||||||
logMapData['reportId'] = data.reportId;
|
|
||||||
logMapData['serverConfigName'] = serverName;
|
|
||||||
// Store API/FTP results as JSON strings
|
|
||||||
logMapData['api_status'] = jsonEncode(apiResults); // Ensure apiResults is a list
|
|
||||||
logMapData['ftp_status'] = jsonEncode(ftpStatuses); // Ensure ftpStatuses is a list
|
|
||||||
|
|
||||||
String? savedLogPath = logDirectory;
|
|
||||||
|
|
||||||
// Save or Update local log file (data.json)
|
|
||||||
if (savedLogPath != null && savedLogPath.isNotEmpty) {
|
|
||||||
// Update existing log
|
|
||||||
logMapData['logDirectory'] = savedLogPath; // Ensure logDirectory path is in the map for update method
|
|
||||||
// *** MODIFIED: Use correct update method ***
|
|
||||||
await _localStorageService.updateRiverInvestigativeLog(logMapData); // NEW UPDATE METHOD
|
|
||||||
} else {
|
|
||||||
// Save new log and get the path
|
|
||||||
// *** MODIFIED: Use correct save method ***
|
|
||||||
savedLogPath = await _localStorageService.saveRiverInvestigativeSamplingData(data, serverName: serverName); // NEW SAVE METHOD
|
|
||||||
if (savedLogPath != null) {
|
|
||||||
logMapData['logDirectory'] = savedLogPath; // Add the new path for central log
|
|
||||||
} else {
|
|
||||||
debugPrint("Failed to save River Investigative log locally, central DB log might be incomplete.");
|
|
||||||
// Handle case where local save failed? Maybe skip central log or log with error?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Save record to central DB log (submission_log table)
|
|
||||||
final imagePaths = data.toApiImageFiles().values.whereType<File>().map((f) => f.path).toList();
|
|
||||||
final centralLogData = {
|
|
||||||
'submission_id': data.reportId ?? baseFileName, // Use report ID or generated name as unique ID
|
|
||||||
// *** MODIFIED: Module and Type ***
|
|
||||||
'module': 'river', // Keep main module as 'river'
|
|
||||||
'type': 'Investigative', // Specific type
|
|
||||||
'status': status,
|
|
||||||
'message': message,
|
|
||||||
'report_id': data.reportId,
|
|
||||||
'created_at': DateTime.now().toIso8601String(),
|
|
||||||
'form_data': jsonEncode(logMapData), // Log the comprehensive map including paths and status
|
|
||||||
'image_data': jsonEncode(imagePaths), // Log original image paths used for submission attempt
|
|
||||||
'server_name': serverName,
|
|
||||||
'api_status': jsonEncode(apiResults), // Log API results
|
|
||||||
'ftp_status': jsonEncode(ftpStatuses), // Log FTP results
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
await _dbHelper.saveSubmissionLog(centralLogData);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Error saving River Investigative submission log to DB: $e"); // Log context update
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Handles sending or queuing the Telegram alert for River Investigative submissions.
|
|
||||||
Future<void> _handleSuccessAlert(RiverInvesManualSamplingData data, List<Map<String, dynamic>>? appSettings, {required bool isDataOnly, bool isSessionExpired = false}) async { // Updated model type
|
|
||||||
try {
|
|
||||||
final message = await _generateInvestigativeAlertMessage(data, isDataOnly: isDataOnly); // Call specific helper
|
|
||||||
// *** MODIFIED: Telegram key ***
|
|
||||||
final alertKey = 'river_investigative'; // Specific key for this module
|
|
||||||
|
|
||||||
if (isSessionExpired) {
|
|
||||||
debugPrint("Session is expired; queuing River Investigative Telegram alert directly for $alertKey."); // Log context update
|
|
||||||
await _telegramService.queueMessage(alertKey, message, appSettings);
|
|
||||||
} else {
|
|
||||||
final bool wasSent = await _telegramService.sendAlertImmediately(alertKey, message, appSettings);
|
|
||||||
if (!wasSent) {
|
|
||||||
// Fallback to queueing if immediate send fails
|
|
||||||
await _telegramService.queueMessage(alertKey, message, appSettings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Failed to handle River Investigative Telegram alert: $e"); // Log context update
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: NEW HELPER METHOD (for timestamp ID) ---
|
||||||
/// Generates the specific Telegram alert message content for River Investigative.
|
/// Generates the specific Telegram alert message content for River Investigative.
|
||||||
Future<String> _generateInvestigativeAlertMessage(RiverInvesManualSamplingData data, {required bool isDataOnly}) async { // Updated model type
|
Future<String> _generateInvestigativeAlertMessage(RiverInvesManualSamplingData data, {required bool isDataOnly}) async { // Updated model type
|
||||||
final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)";
|
final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)";
|
||||||
@ -692,6 +576,188 @@ class RiverInvestigativeSamplingService { // Renamed class
|
|||||||
|
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
// --- END: NEW HELPER METHOD ---
|
||||||
|
|
||||||
|
// --- START: MODIFIED _generateBaseFileName ---
|
||||||
|
/// Helper to generate the base filename for ZIP files (Investigative).
|
||||||
|
String _generateBaseFileName(RiverInvesManualSamplingData data) { // Updated model type
|
||||||
|
// Use the determined station code helper
|
||||||
|
final stationCode = data.getDeterminedStationCode() ?? 'UNKNOWN';
|
||||||
|
|
||||||
|
// We now always use data.reportId, which we set as the timestamp.
|
||||||
|
if (data.reportId == null || data.reportId!.isEmpty) {
|
||||||
|
// This is a safety fallback, but should not happen if submitData is used.
|
||||||
|
debugPrint("Warning: reportId is null in _generateBaseFileName. Using current timestamp.");
|
||||||
|
return '${stationCode}_${DateTime.now().millisecondsSinceEpoch.toString()}';
|
||||||
|
}
|
||||||
|
return "${stationCode}_${data.reportId}"; // Consistent format
|
||||||
|
}
|
||||||
|
// --- 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
|
||||||
|
final baseFileName = _generateBaseFileName(data); // Use helper
|
||||||
|
|
||||||
|
// *** MODIFIED: Use correct base dir getter ***
|
||||||
|
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;
|
||||||
|
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)
|
||||||
|
final dataZip = await _zippingService.createDataZip(
|
||||||
|
jsonDataMap: {
|
||||||
|
// *** MODIFIED: Use Investigative model's JSON methods and filenames ***
|
||||||
|
'db.json': data.toDbJson(), // Main data structure
|
||||||
|
'river_inves_basic_form.json': data.toBasicFormJson(),
|
||||||
|
'river_inves_reading.json': data.toReadingJson(),
|
||||||
|
'river_inves_manual_info.json': data.toManualInfoJson(),
|
||||||
|
},
|
||||||
|
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(
|
||||||
|
moduleName: moduleName, // 'river_investigative'
|
||||||
|
fileToUpload: dataZip,
|
||||||
|
remotePath: '/${p.basename(dataZip.path)}' // Standard remote path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
baseFileName: baseFileName,
|
||||||
|
destinationDir: localSubmissionDir, // Save ZIP in the specific log folder
|
||||||
|
);
|
||||||
|
if (imageZip != null) {
|
||||||
|
ftpImageResult = await _submissionFtpService.submit(
|
||||||
|
moduleName: moduleName, // 'river_investigative'
|
||||||
|
fileToUpload: imageZip,
|
||||||
|
remotePath: '/${p.basename(imageZip.path)}' // Standard remote path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Combine statuses from both uploads
|
||||||
|
return {
|
||||||
|
'statuses': <Map<String, dynamic>>[
|
||||||
|
...(ftpDataResult['statuses'] as List? ?? []), // Use null-aware spread
|
||||||
|
...(ftpImageResult['statuses'] as List? ?? []), // Use null-aware spread
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves or updates the local log file and saves a record to the central DB log (Investigative).
|
||||||
|
Future<void> _logAndSave({
|
||||||
|
required RiverInvesManualSamplingData data, // Updated model type
|
||||||
|
required String status,
|
||||||
|
required String message,
|
||||||
|
required List<Map<String, dynamic>> apiResults,
|
||||||
|
required List<Map<String, dynamic>> ftpStatuses,
|
||||||
|
required String serverName,
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
String? apiRecordId, // The server's DB ID (e.g., 102)
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
String? logDirectory, // Can be null initially, gets populated on first save
|
||||||
|
}) async {
|
||||||
|
data.submissionStatus = status;
|
||||||
|
data.submissionMessage = message;
|
||||||
|
final baseFileName = _generateBaseFileName(data); // Use helper for consistent naming
|
||||||
|
|
||||||
|
// Prepare log data map using toMap()
|
||||||
|
final Map<String, dynamic> logMapData = data.toMap();
|
||||||
|
// Add submission metadata that might not be in toMap() or needs overriding
|
||||||
|
logMapData['submissionStatus'] = status;
|
||||||
|
logMapData['submissionMessage'] = message;
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// data.reportId (the timestamp) is already in the map from toMap()
|
||||||
|
logMapData['apiRecordId'] = apiRecordId; // Add the server DB ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
logMapData['serverConfigName'] = serverName;
|
||||||
|
// Store API/FTP results as JSON strings
|
||||||
|
logMapData['api_status'] = jsonEncode(apiResults); // Ensure apiResults is a list
|
||||||
|
logMapData['ftp_status'] = jsonEncode(ftpStatuses); // Ensure ftpStatuses is a list
|
||||||
|
|
||||||
|
String? savedLogPath = logDirectory;
|
||||||
|
|
||||||
|
// Save or Update local log file (data.json)
|
||||||
|
if (savedLogPath != null && savedLogPath.isNotEmpty) {
|
||||||
|
// Update existing log
|
||||||
|
logMapData['logDirectory'] = savedLogPath; // Ensure logDirectory path is in the map for update method
|
||||||
|
// *** MODIFIED: Use correct update method ***
|
||||||
|
await _localStorageService.updateRiverInvestigativeLog(logMapData); // NEW UPDATE METHOD
|
||||||
|
} else {
|
||||||
|
// Save new log and get the path
|
||||||
|
// *** MODIFIED: Use correct save method ***
|
||||||
|
savedLogPath = await _localStorageService.saveRiverInvestigativeSamplingData(data, serverName: serverName); // NEW SAVE METHOD
|
||||||
|
if (savedLogPath != null) {
|
||||||
|
logMapData['logDirectory'] = savedLogPath; // Add the new path for central log
|
||||||
|
} else {
|
||||||
|
debugPrint("Failed to save River Investigative log locally, central DB log might be incomplete.");
|
||||||
|
// Handle case where local save failed? Maybe skip central log or log with error?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Save record to central DB log (submission_log table)
|
||||||
|
final imagePaths = data.toApiImageFiles().values.whereType<File>().map((f) => f.path).toList();
|
||||||
|
final centralLogData = {
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
'submission_id': data.reportId ?? baseFileName, // Use timestamp ID
|
||||||
|
// *** MODIFIED: Module and Type ***
|
||||||
|
'module': 'river', // Keep main module as 'river'
|
||||||
|
'type': 'Investigative', // Specific type
|
||||||
|
'status': status,
|
||||||
|
'message': message,
|
||||||
|
'report_id': apiRecordId, // Use server DB ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
|
'form_data': jsonEncode(logMapData), // Log the comprehensive map including paths and status
|
||||||
|
'image_data': jsonEncode(imagePaths), // Log original image paths used for submission attempt
|
||||||
|
'server_name': serverName,
|
||||||
|
'api_status': jsonEncode(apiResults), // Log API results
|
||||||
|
'ftp_status': jsonEncode(ftpStatuses), // Log FTP results
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await _dbHelper.saveSubmissionLog(centralLogData);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error saving River Investigative submission log to DB: $e"); // Log context update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Handles sending or queuing the Telegram alert for River Investigative submissions.
|
||||||
|
Future<void> _handleSuccessAlert(RiverInvesManualSamplingData data, List<Map<String, dynamic>>? appSettings, {required bool isDataOnly, bool isSessionExpired = false}) async { // Updated model type
|
||||||
|
try {
|
||||||
|
final message = await _generateInvestigativeAlertMessage(data, isDataOnly: isDataOnly); // Call specific helper
|
||||||
|
// *** MODIFIED: Telegram key ***
|
||||||
|
final alertKey = 'river_investigative'; // Specific key for this module
|
||||||
|
|
||||||
|
if (isSessionExpired) {
|
||||||
|
debugPrint("Session is expired; queuing River Investigative Telegram alert directly for $alertKey."); // Log context update
|
||||||
|
await _telegramService.queueMessage(alertKey, message, appSettings);
|
||||||
|
} else {
|
||||||
|
final bool wasSent = await _telegramService.sendAlertImmediately(alertKey, message, appSettings);
|
||||||
|
if (!wasSent) {
|
||||||
|
// Fallback to queueing if immediate send fails
|
||||||
|
await _telegramService.queueMessage(alertKey, message, appSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Failed to handle River Investigative Telegram alert: $e"); // Log context update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper to generate the parameter limit alert section for Telegram (River Investigative).
|
/// Helper to generate the parameter limit alert section for Telegram (River Investigative).
|
||||||
Future<String> _getOutOfBoundsAlertSection(RiverInvesManualSamplingData data) async { // Updated model type
|
Future<String> _getOutOfBoundsAlertSection(RiverInvesManualSamplingData data) async { // Updated model type
|
||||||
|
|||||||
@ -163,6 +163,22 @@ class RiverManualTriennialSamplingService {
|
|||||||
_serialManager.dispose();
|
_serialManager.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: NEW HELPER METHOD ---
|
||||||
|
/// Generates a unique timestamp ID from the sampling date and time.
|
||||||
|
String _generateTimestampId(String? date, String? time) {
|
||||||
|
final String dateTimeString = "${date ?? ''} ${time ?? ''}";
|
||||||
|
try {
|
||||||
|
// Time format from model is HH:mm
|
||||||
|
final DateTime samplingDateTime = DateFormat('yyyy-MM-dd HH:mm').parse(dateTimeString);
|
||||||
|
return samplingDateTime.millisecondsSinceEpoch.toString();
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback: if parsing fails, use the current time in milliseconds
|
||||||
|
debugPrint("Could not parse '$dateTimeString' for timestamp ID, using current time. Error: $e");
|
||||||
|
return DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END: NEW HELPER METHOD ---
|
||||||
|
|
||||||
Future<Map<String, dynamic>> submitData({
|
Future<Map<String, dynamic>> submitData({
|
||||||
required RiverManualTriennialSamplingData data,
|
required RiverManualTriennialSamplingData data,
|
||||||
required List<Map<String, dynamic>>? appSettings,
|
required List<Map<String, dynamic>>? appSettings,
|
||||||
@ -171,6 +187,13 @@ class RiverManualTriennialSamplingService {
|
|||||||
}) async {
|
}) async {
|
||||||
const String moduleName = 'river_triennial'; // Correct module name
|
const String moduleName = 'river_triennial'; // Correct module name
|
||||||
|
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// Generate the unique timestamp ID and assign it immediately.
|
||||||
|
// This ID will be used for filenames and as the primary submission ID.
|
||||||
|
final String timestampId = _generateTimestampId(data.samplingDate, data.samplingTime);
|
||||||
|
data.reportId = timestampId; // This is the primary ID now.
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
final connectivityResult = await Connectivity().checkConnectivity();
|
final connectivityResult = await Connectivity().checkConnectivity();
|
||||||
bool isOnline = !connectivityResult.contains(ConnectivityResult.none);
|
bool isOnline = !connectivityResult.contains(ConnectivityResult.none);
|
||||||
bool isOfflineSession = authProvider.isLoggedIn && (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false);
|
bool isOfflineSession = authProvider.isLoggedIn && (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false);
|
||||||
@ -228,6 +251,11 @@ class RiverManualTriennialSamplingService {
|
|||||||
String finalStatus = '';
|
String finalStatus = '';
|
||||||
bool isSessionKnownToBeExpired = false;
|
bool isSessionKnownToBeExpired = false;
|
||||||
|
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
String? apiRecordId; // Will hold the DB ID (e.g., 102) from the server
|
||||||
|
// data.reportId already contains the timestamp ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Submit Form Data
|
// 1. Submit Form Data
|
||||||
apiDataResult = await _submissionApiService.submitPost(
|
apiDataResult = await _submissionApiService.submitPost(
|
||||||
@ -238,15 +266,20 @@ class RiverManualTriennialSamplingService {
|
|||||||
|
|
||||||
if (apiDataResult['success'] == true) {
|
if (apiDataResult['success'] == true) {
|
||||||
anyApiSuccess = true;
|
anyApiSuccess = true;
|
||||||
data.reportId = apiDataResult['data']?['r_tri_id']?.toString(); // Correct ID key
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// Store the server's database ID in a separate variable.
|
||||||
|
apiRecordId = apiDataResult['data']?['r_tri_id']?.toString(); // Correct ID key
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
if (data.reportId != null) {
|
if (apiRecordId != null) {
|
||||||
if (finalImageFiles.isNotEmpty) {
|
if (finalImageFiles.isNotEmpty) {
|
||||||
// 2. Submit Images
|
// 2. Submit Images
|
||||||
apiImageResult = await _submissionApiService.submitMultipart(
|
apiImageResult = await _submissionApiService.submitMultipart(
|
||||||
moduleName: moduleName,
|
moduleName: moduleName,
|
||||||
endpoint: 'river/triennial/images', // Correct endpoint
|
endpoint: 'river/triennial/images', // Correct endpoint
|
||||||
fields: {'r_tri_id': data.reportId!}, // Correct field key
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
fields: {'r_tri_id': apiRecordId}, // Correct field key
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
files: finalImageFiles,
|
files: finalImageFiles,
|
||||||
);
|
);
|
||||||
if (apiImageResult['success'] != true) {
|
if (apiImageResult['success'] != true) {
|
||||||
@ -255,7 +288,9 @@ class RiverManualTriennialSamplingService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
anyApiSuccess = false;
|
anyApiSuccess = false;
|
||||||
apiDataResult['message'] = 'API Error: Submission succeeded but did not return a record ID.';
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
apiDataResult['message'] = 'API Error: Submission succeeded but did not return a server record ID.';
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If apiDataResult['success'] is false, SubmissionApiService queued it.
|
// If apiDataResult['success'] is false, SubmissionApiService queued it.
|
||||||
@ -267,9 +302,11 @@ class RiverManualTriennialSamplingService {
|
|||||||
apiDataResult = {'success': false, 'message': 'Session expired and re-login failed. API submission queued.'};
|
apiDataResult = {'success': false, 'message': 'Session expired and re-login failed. API submission queued.'};
|
||||||
// Manually queue API calls
|
// Manually queue API calls
|
||||||
await _retryService.addApiToQueue(endpoint: 'river/triennial/sample', method: 'POST', body: data.toApiFormData());
|
await _retryService.addApiToQueue(endpoint: 'river/triennial/sample', method: 'POST', body: data.toApiFormData());
|
||||||
if (finalImageFiles.isNotEmpty && data.reportId != null) {
|
if (finalImageFiles.isNotEmpty && apiRecordId != null) {
|
||||||
// Also queue images if data call might have partially succeeded before expiry
|
// Also queue images if data call might have partially succeeded before expiry
|
||||||
await _retryService.addApiToQueue(endpoint: 'river/triennial/images', method: 'POST_MULTIPART', fields: {'r_tri_id': data.reportId!}, files: finalImageFiles);
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
await _retryService.addApiToQueue(endpoint: 'river/triennial/images', method: 'POST_MULTIPART', fields: {'r_tri_id': apiRecordId}, files: finalImageFiles);
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,7 +316,9 @@ class RiverManualTriennialSamplingService {
|
|||||||
|
|
||||||
if (isSessionKnownToBeExpired) {
|
if (isSessionKnownToBeExpired) {
|
||||||
debugPrint("Skipping FTP attempt due to known expired session. Manually queuing FTP tasks.");
|
debugPrint("Skipping FTP attempt due to known expired session. Manually queuing FTP tasks.");
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
final baseFileNameForQueue = _generateBaseFileName(data); // Use helper
|
final baseFileNameForQueue = _generateBaseFileName(data); // Use helper
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
|
||||||
// --- START FIX: Add ftpConfigId when queuing ---
|
// --- START FIX: Add ftpConfigId when queuing ---
|
||||||
// Get all potential FTP configs
|
// Get all potential FTP configs
|
||||||
@ -365,6 +404,9 @@ class RiverManualTriennialSamplingService {
|
|||||||
apiResults: [apiDataResult, apiImageResult],
|
apiResults: [apiDataResult, apiImageResult],
|
||||||
ftpStatuses: ftpResults['statuses'],
|
ftpStatuses: ftpResults['statuses'],
|
||||||
serverName: serverName,
|
serverName: serverName,
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
apiRecordId: apiRecordId, // Pass the server DB ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
logDirectory: logDirectory,
|
logDirectory: logDirectory,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -378,7 +420,9 @@ class RiverManualTriennialSamplingService {
|
|||||||
'status': finalStatus,
|
'status': finalStatus,
|
||||||
'success': overallSuccess,
|
'success': overallSuccess,
|
||||||
'message': finalMessage,
|
'message': finalMessage,
|
||||||
'reportId': data.reportId
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
'reportId': data.reportId // This is now the timestamp ID
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,7 +452,9 @@ class RiverManualTriennialSamplingService {
|
|||||||
|
|
||||||
if (savedLogPath == null) {
|
if (savedLogPath == null) {
|
||||||
const message = "Failed to save submission to local device storage.";
|
const message = "Failed to save submission to local device storage.";
|
||||||
await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, logDirectory: logDirectory);
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, apiRecordId: null, logDirectory: logDirectory);
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
return {'status': 'Error', 'success': false, 'message': message};
|
return {'status': 'Error', 'success': false, 'message': message};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -423,17 +469,25 @@ class RiverManualTriennialSamplingService {
|
|||||||
|
|
||||||
const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
|
const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
|
||||||
// Log final queued state to central DB
|
// Log final queued state to central DB
|
||||||
// await _logAndSave(data: data, status: 'Queued', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, logDirectory: savedLogPath);
|
// await _logAndSave(data: data, status: 'Queued', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, apiRecordId: null, logDirectory: savedLogPath);
|
||||||
|
|
||||||
return {'status': 'Queued', 'success': true, 'message': successMessage, 'reportId': null};
|
return {'status': 'Queued', 'success': true, 'message': successMessage, 'reportId': data.reportId}; // Return timestamp ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: MODIFIED _generateBaseFileName ---
|
||||||
/// Helper to generate the base filename for ZIP files.
|
/// Helper to generate the base filename for ZIP files.
|
||||||
String _generateBaseFileName(RiverManualTriennialSamplingData data) {
|
String _generateBaseFileName(RiverManualTriennialSamplingData data) {
|
||||||
final stationCode = data.selectedStation?['sampling_station_code'] ?? 'UNKNOWN';
|
final stationCode = data.selectedStation?['sampling_station_code'] ?? 'UNKNOWN';
|
||||||
final fileTimestamp = "${data.samplingDate}_${data.samplingTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
|
||||||
return "${stationCode}_$fileTimestamp";
|
// We now always use data.reportId, which we set as the timestamp.
|
||||||
|
if (data.reportId == null || data.reportId!.isEmpty) {
|
||||||
|
// This is a safety fallback, but should not happen if submitData is used.
|
||||||
|
debugPrint("Warning: reportId is null in _generateBaseFileName. Using current timestamp.");
|
||||||
|
return '${stationCode}_${DateTime.now().millisecondsSinceEpoch.toString()}';
|
||||||
|
}
|
||||||
|
return '${stationCode}_${data.reportId}';
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED _generateBaseFileName ---
|
||||||
|
|
||||||
|
|
||||||
/// Generates data and image ZIP files and uploads them using SubmissionFtpService.
|
/// Generates data and image ZIP files and uploads them using SubmissionFtpService.
|
||||||
@ -446,7 +500,9 @@ class RiverManualTriennialSamplingService {
|
|||||||
subModule: 'river_triennial_sampling', // Correct sub-module path
|
subModule: 'river_triennial_sampling', // Correct sub-module path
|
||||||
);
|
);
|
||||||
|
|
||||||
final folderName = data.reportId ?? baseFileName;
|
// --- 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, folderName)) : null;
|
||||||
if (localSubmissionDir != null && !await localSubmissionDir.exists()) {
|
if (localSubmissionDir != null && !await localSubmissionDir.exists()) {
|
||||||
await localSubmissionDir.create(recursive: true);
|
await localSubmissionDir.create(recursive: true);
|
||||||
@ -492,18 +548,25 @@ class RiverManualTriennialSamplingService {
|
|||||||
required List<Map<String, dynamic>> apiResults,
|
required List<Map<String, dynamic>> apiResults,
|
||||||
required List<Map<String, dynamic>> ftpStatuses,
|
required List<Map<String, dynamic>> ftpStatuses,
|
||||||
required String serverName,
|
required String serverName,
|
||||||
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
String? apiRecordId, // This is the server DB ID (e.g., 102)
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
String? logDirectory,
|
String? logDirectory,
|
||||||
}) async {
|
}) async {
|
||||||
data.submissionStatus = status;
|
data.submissionStatus = status;
|
||||||
data.submissionMessage = message;
|
data.submissionMessage = message;
|
||||||
final baseFileName = _generateBaseFileName(data); // Use helper
|
final baseFileName = _generateBaseFileName(data); // This now uses the timestamp ID
|
||||||
|
|
||||||
// Prepare log data map using toMap()
|
// Prepare log data map using toMap()
|
||||||
final Map<String, dynamic> logMapData = data.toMap();
|
final Map<String, dynamic> logMapData = data.toMap();
|
||||||
// Add submission metadata
|
// Add submission metadata
|
||||||
logMapData['submissionStatus'] = status;
|
logMapData['submissionStatus'] = status;
|
||||||
logMapData['submissionMessage'] = message;
|
logMapData['submissionMessage'] = message;
|
||||||
logMapData['reportId'] = data.reportId;
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
// data.reportId (which is the timestamp) is already in the map from toMap()
|
||||||
|
// We add the server's DB ID separately for the log file
|
||||||
|
logMapData['apiRecordId'] = apiRecordId; // (e.g., 102)
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
logMapData['serverConfigName'] = serverName;
|
logMapData['serverConfigName'] = serverName;
|
||||||
logMapData['api_status'] = jsonEncode(apiResults.where((r) => r.isNotEmpty).toList());
|
logMapData['api_status'] = jsonEncode(apiResults.where((r) => r.isNotEmpty).toList());
|
||||||
logMapData['ftp_status'] = jsonEncode(ftpStatuses);
|
logMapData['ftp_status'] = jsonEncode(ftpStatuses);
|
||||||
@ -521,12 +584,14 @@ class RiverManualTriennialSamplingService {
|
|||||||
// Save to central DB log
|
// Save to central DB log
|
||||||
final imagePaths = data.toApiImageFiles().values.whereType<File>().map((f) => f.path).toList();
|
final imagePaths = data.toApiImageFiles().values.whereType<File>().map((f) => f.path).toList();
|
||||||
final centralLogData = {
|
final centralLogData = {
|
||||||
'submission_id': data.reportId ?? baseFileName, // Use helper result
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
|
'submission_id': data.reportId ?? baseFileName, // This is the timestamp ID
|
||||||
'module': 'river',
|
'module': 'river',
|
||||||
'type': data.samplingType ?? 'Triennial', // Correct type
|
'type': data.samplingType ?? 'Triennial', // Correct type
|
||||||
'status': status,
|
'status': status,
|
||||||
'message': message,
|
'message': message,
|
||||||
'report_id': data.reportId,
|
'report_id': apiRecordId, // This is the server DB ID (e.g., 102)
|
||||||
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
||||||
'created_at': DateTime.now().toIso8601String(),
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
'form_data': jsonEncode(logMapData), // Log the comprehensive map
|
'form_data': jsonEncode(logMapData), // Log the comprehensive map
|
||||||
'image_data': jsonEncode(imagePaths),
|
'image_data': jsonEncode(imagePaths),
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:convert'; // Added to ensure correct UTF-8 encoding
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:archive/archive_io.dart';
|
import 'package:archive/archive_io.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@ -30,8 +31,15 @@ class ZippingService {
|
|||||||
final fileName = entry.key;
|
final fileName = entry.key;
|
||||||
final jsonContent = entry.value;
|
final jsonContent = entry.value;
|
||||||
|
|
||||||
// --- MODIFIED: The codeUnits property is already a List<int>. No need to wrap it in a Stream. ---
|
// --- MODIFIED: Ensure UTF-8 encoding ---
|
||||||
final archiveFile = ArchiveFile(fileName, jsonContent.length, jsonContent.codeUnits);
|
// 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);
|
encoder.addArchiveFile(archiveFile);
|
||||||
|
|
||||||
debugPrint("Added $fileName to data ZIP.");
|
debugPrint("Added $fileName to data ZIP.");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user