584 lines
26 KiB
Dart
584 lines
26 KiB
Dart
// lib/services/marine_tarball_sampling_service.dart
|
|
|
|
import 'dart:io';
|
|
import 'dart:convert';
|
|
import 'dart:async'; // Added for TimeoutException
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:intl/intl.dart'; // <-- Import intl
|
|
|
|
import 'package:environment_monitoring_app/models/tarball_data.dart';
|
|
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
|
import 'package:environment_monitoring_app/services/server_config_service.dart';
|
|
import 'package:environment_monitoring_app/services/zipping_service.dart';
|
|
//import 'packagepackage:environment_monitoring_app/services/api_service.dart';
|
|
import 'package:environment_monitoring_app/services/database_helper.dart';
|
|
|
|
import 'package:environment_monitoring_app/services/submission_api_service.dart';
|
|
import 'package:environment_monitoring_app/services/submission_ftp_service.dart';
|
|
import 'package:environment_monitoring_app/services/telegram_service.dart';
|
|
import 'package:environment_monitoring_app/services/retry_service.dart';
|
|
import 'package:environment_monitoring_app/auth_provider.dart';
|
|
import 'package:environment_monitoring_app/services/base_api_service.dart'; // Import for SessionExpiredException
|
|
|
|
/// A dedicated service to handle all business logic for the Marine Tarball Sampling feature.
|
|
class MarineTarballSamplingService {
|
|
final SubmissionApiService _submissionApiService = SubmissionApiService();
|
|
final SubmissionFtpService _submissionFtpService = SubmissionFtpService();
|
|
final ZippingService _zippingService = ZippingService();
|
|
final LocalStorageService _localStorageService = LocalStorageService();
|
|
final ServerConfigService _serverConfigService = ServerConfigService();
|
|
final DatabaseHelper _dbHelper = DatabaseHelper();
|
|
final RetryService _retryService = RetryService();
|
|
final TelegramService _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({
|
|
required TarballSamplingData data,
|
|
required List<Map<String, dynamic>>? appSettings,
|
|
// --- START FIX: Make BuildContext nullable ---
|
|
required BuildContext? context,
|
|
// --- END FIX ---
|
|
String? logDirectory, // Added for retry consistency
|
|
}) async {
|
|
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 ---
|
|
final authProvider = context != null ? Provider.of<AuthProvider>(context, listen: false) : null;
|
|
// Need a fallback mechanism if context is null (e.g., during retry)
|
|
// One option is to ensure AuthProvider is always accessible, maybe via a singleton or passed differently.
|
|
// For now, we'll proceed assuming authProvider might be null during retry,
|
|
// which could affect session checks. Consider injecting AuthProvider if needed globally.
|
|
if (authProvider == null && context != null) {
|
|
// If context was provided but provider failed, log error
|
|
debugPrint("Error: AuthProvider not found in context for Tarball submission.");
|
|
return {'success': false, 'message': 'Internal error: AuthProvider not available.'};
|
|
}
|
|
// --- END FIX ---
|
|
|
|
final connectivityResult = await Connectivity().checkConnectivity();
|
|
bool isOnline = !connectivityResult.contains(ConnectivityResult.none);
|
|
// --- START FIX: Handle potentially null authProvider ---
|
|
bool isOfflineSession = authProvider != null && authProvider.isLoggedIn && (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false);
|
|
|
|
if (isOnline && isOfflineSession && authProvider != null) {
|
|
debugPrint("Tarball submission online during an offline session. Attempting auto-relogin...");
|
|
try {
|
|
final bool transitionSuccess = await authProvider.checkAndTransitionToOnlineSession();
|
|
if (transitionSuccess) {
|
|
isOfflineSession = false;
|
|
} else {
|
|
isOnline = false; // Auto-relogin failed, treat as offline
|
|
}
|
|
} on SessionExpiredException catch (_) {
|
|
debugPrint("Session expired during auto-relogin check. Treating as offline.");
|
|
isOnline = false;
|
|
}
|
|
}
|
|
// --- END FIX ---
|
|
|
|
|
|
if (isOnline && !isOfflineSession) {
|
|
debugPrint("Proceeding with direct ONLINE Tarball submission...");
|
|
return await _performOnlineSubmission(
|
|
data: data,
|
|
appSettings: appSettings,
|
|
moduleName: moduleName,
|
|
authProvider: authProvider, // Pass potentially null provider
|
|
logDirectory: logDirectory,
|
|
);
|
|
} else {
|
|
debugPrint("Proceeding with OFFLINE Tarball queuing mechanism...");
|
|
return await _performOfflineQueuing(
|
|
data: data,
|
|
moduleName: moduleName,
|
|
logDirectory: logDirectory, // Pass logDirectory for potential update
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<Map<String, dynamic>> _performOnlineSubmission({
|
|
required TarballSamplingData data,
|
|
required List<Map<String, dynamic>>? appSettings,
|
|
required String moduleName,
|
|
required AuthProvider? authProvider, // Accept potentially null provider
|
|
String? logDirectory, // Added for retry consistency
|
|
}) async {
|
|
final serverName = (await _serverConfigService.getActiveApiConfig())?['config_name'] as String? ?? 'Default';
|
|
final imageFiles = data.toImageFiles()..removeWhere((key, value) => value == null);
|
|
final finalImageFiles = imageFiles.cast<String, File>();
|
|
|
|
bool anyApiSuccess = false;
|
|
Map<String, dynamic> apiDataResult = {};
|
|
Map<String, dynamic> apiImageResult = {};
|
|
String finalMessage = '';
|
|
String finalStatus = '';
|
|
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 {
|
|
// 1. Submit Form Data
|
|
apiDataResult = await _submissionApiService.submitPost(
|
|
moduleName: moduleName,
|
|
endpoint: 'marine/tarball/sample', // Correct endpoint
|
|
body: data.toFormData(), // Use specific method for tarball form data
|
|
);
|
|
|
|
if (apiDataResult['success'] == true) {
|
|
anyApiSuccess = true;
|
|
// --- 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 (apiRecordId != null) {
|
|
if (finalImageFiles.isNotEmpty) {
|
|
// 2. Submit Images
|
|
apiImageResult = await _submissionApiService.submitMultipart(
|
|
moduleName: moduleName,
|
|
endpoint: 'marine/tarball/images', // Correct endpoint
|
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
|
fields: {'autoid': apiRecordId}, // Correct field key
|
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
|
files: finalImageFiles,
|
|
);
|
|
if (apiImageResult['success'] != true) {
|
|
anyApiSuccess = false; // Downgrade success if images fail
|
|
}
|
|
}
|
|
// If data succeeded but no images, API part is still successful
|
|
} else {
|
|
anyApiSuccess = false;
|
|
// --- 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.
|
|
|
|
} on SessionExpiredException catch (_) {
|
|
debugPrint("API submission failed with SessionExpiredException during online submission.");
|
|
isSessionKnownToBeExpired = true;
|
|
anyApiSuccess = false;
|
|
apiDataResult = {'success': false, 'message': 'Session expired. API submission queued.'};
|
|
// Manually queue API calls
|
|
await _retryService.addApiToQueue(endpoint: 'marine/tarball/sample', method: 'POST', body: data.toFormData());
|
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
|
if (finalImageFiles.isNotEmpty && apiRecordId != null) {
|
|
// Queue images if data might have partially succeeded
|
|
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
|
|
Map<String, dynamic> ftpResults = {'statuses': []};
|
|
bool anyFtpSuccess = false;
|
|
|
|
if (isSessionKnownToBeExpired) {
|
|
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
|
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
|
|
|
// --- START FIX: Add ftpConfigId when queuing ---
|
|
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
|
|
|
final dataZip = await _zippingService.createDataZip(
|
|
jsonDataMap: { // Use specific JSON structures for Tarball FTP
|
|
'data.json': jsonEncode(data.toDbJson()),
|
|
'basic_form.json': jsonEncode(data.toBasicFormJson()),
|
|
'reading.json': jsonEncode(data.toReadingJson()),
|
|
'manual_info.json': jsonEncode(data.toManualInfoJson()),
|
|
},
|
|
baseFileName: baseFileNameForQueue,
|
|
destinationDir: null,
|
|
);
|
|
if (dataZip != null) {
|
|
// Queue for each config separately
|
|
for (final config in ftpConfigs) {
|
|
final configId = config['ftp_config_id'];
|
|
if (configId != null) {
|
|
await _retryService.addFtpToQueue(
|
|
localFilePath: dataZip.path,
|
|
remotePath: '/${p.basename(dataZip.path)}',
|
|
ftpConfigId: configId // Provide the specific config ID
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (finalImageFiles.isNotEmpty) {
|
|
final imageZip = await _zippingService.createImageZip(
|
|
imageFiles: finalImageFiles.values.toList(),
|
|
baseFileName: baseFileNameForQueue,
|
|
destinationDir: null,
|
|
);
|
|
if (imageZip != null) {
|
|
// Queue for each config separately
|
|
for (final config in ftpConfigs) {
|
|
final configId = config['ftp_config_id'];
|
|
if (configId != null) {
|
|
await _retryService.addFtpToQueue(
|
|
localFilePath: imageZip.path,
|
|
remotePath: '/${p.basename(imageZip.path)}',
|
|
ftpConfigId: configId // Provide the specific config ID
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// --- END FIX ---
|
|
ftpResults = {'statuses': [{'status': 'Queued', 'message': 'FTP upload queued due to API session issue.', 'success': false}]};
|
|
anyFtpSuccess = false;
|
|
} else {
|
|
try {
|
|
ftpResults = await _generateAndUploadFtpFiles(data, finalImageFiles, serverName, moduleName);
|
|
anyFtpSuccess = !(ftpResults['statuses'] as List).any((status) => status['success'] == false && status['status'] != 'Not Configured');
|
|
} catch (e) {
|
|
debugPrint("Unexpected FTP submission error: $e");
|
|
anyFtpSuccess = false;
|
|
}
|
|
}
|
|
|
|
|
|
// 4. Determine Final Status
|
|
final bool overallSuccess = anyApiSuccess || anyFtpSuccess;
|
|
|
|
if (anyApiSuccess && anyFtpSuccess) {
|
|
finalMessage = 'Data submitted successfully to all destinations.';
|
|
finalStatus = 'S4';
|
|
} else if (anyApiSuccess && !anyFtpSuccess) {
|
|
finalMessage = 'Data sent to API, but some FTP uploads failed or were queued.';
|
|
finalStatus = 'S3';
|
|
} else if (!anyApiSuccess && anyFtpSuccess) {
|
|
finalMessage = 'API submission failed and was queued, but files were sent to FTP successfully.';
|
|
finalStatus = 'L4';
|
|
} else {
|
|
finalMessage = apiDataResult['message'] ?? 'All submission attempts failed and have been queued for retry.';
|
|
finalStatus = 'L1';
|
|
}
|
|
|
|
// 5. Log Locally
|
|
await _logAndSave(
|
|
data: data,
|
|
status: finalStatus,
|
|
message: finalMessage,
|
|
apiResults: [apiDataResult, apiImageResult],
|
|
ftpStatuses: ftpResults['statuses'],
|
|
serverName: serverName,
|
|
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
|
|
);
|
|
|
|
// 6. Send Alert
|
|
if (overallSuccess) {
|
|
_handleTarballSuccessAlert(data, appSettings, isDataOnly: finalImageFiles.isEmpty, isSessionExpired: isSessionKnownToBeExpired);
|
|
}
|
|
|
|
return {'success': overallSuccess, 'message': finalMessage, 'reportId': data.reportId}; // Return timestamp ID
|
|
}
|
|
|
|
Future<Map<String, dynamic>> _performOfflineQueuing({
|
|
required TarballSamplingData data,
|
|
required String moduleName,
|
|
String? logDirectory, // Added for potential update
|
|
}) async {
|
|
final serverConfig = await _serverConfigService.getActiveApiConfig();
|
|
final serverName = serverConfig?['config_name'] as String? ?? 'Default';
|
|
|
|
// Set status before saving/updating
|
|
data.submissionStatus = 'L1'; // Logged locally or Queued
|
|
data.submissionMessage = 'Submission queued for later retry.';
|
|
|
|
String? savedLogPath = logDirectory; // Use existing path if provided
|
|
|
|
// Save/Update local log first
|
|
if (savedLogPath != null && savedLogPath.isNotEmpty) {
|
|
await _localStorageService.updateTarballLog(data.toDbJson()..['logDirectory'] = savedLogPath);
|
|
debugPrint("Updated existing Tarball log for queuing: $savedLogPath");
|
|
} else {
|
|
savedLogPath = await _localStorageService.saveTarballSamplingData(data, serverName: serverName);
|
|
debugPrint("Saved new Tarball log for queuing: $savedLogPath");
|
|
}
|
|
|
|
|
|
if (savedLogPath == null) {
|
|
const message = "Failed to save submission to local device storage.";
|
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
|
// Log failure state if saving fails
|
|
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};
|
|
}
|
|
|
|
// Queue a single task for the RetryService
|
|
await _retryService.queueTask(
|
|
type: 'tarball_submission', // Use specific type
|
|
payload: {
|
|
'module': moduleName,
|
|
'localLogPath': savedLogPath, // Point retry service to the saved log *directory*
|
|
'serverConfig': serverConfig,
|
|
},
|
|
);
|
|
|
|
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
|
|
// await _logAndSave(data: data, status: 'Queued', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}, apiRecordId: null, logDirectory: savedLogPath);
|
|
|
|
return {'success': true, 'message': successMessage, 'reportId': data.reportId}; // Return timestamp ID
|
|
}
|
|
|
|
// --- START: MODIFIED _generateBaseFileName ---
|
|
/// Helper to generate the base filename for ZIP files.
|
|
String _generateBaseFileName(TarballSamplingData data) {
|
|
final stationCode = data.selectedStation?['tbl_station_code'] ?? 'NA';
|
|
|
|
// 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.
|
|
Future<Map<String, dynamic>> _generateAndUploadFtpFiles(TarballSamplingData data, Map<String, File> imageFiles, String serverName, String moduleName) async {
|
|
final baseFileName = _generateBaseFileName(data);
|
|
|
|
final Directory? logDirectory = await _localStorageService.getLogDirectory(
|
|
serverName: serverName,
|
|
module: 'marine',
|
|
subModule: 'marine_tarball_sampling', // Correct sub-module
|
|
);
|
|
// --- 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 and upload data ZIP (with multiple JSON files specific to Tarball)
|
|
final dataZip = await _zippingService.createDataZip(
|
|
jsonDataMap: {
|
|
'data.json': jsonEncode(data.toDbJson()), // Use specific method
|
|
'basic_form.json': jsonEncode(data.toBasicFormJson()), // Use specific method
|
|
'reading.json': jsonEncode(data.toReadingJson()), // Use specific method
|
|
'manual_info.json': jsonEncode(data.toManualInfoJson()), // Use specific method
|
|
},
|
|
baseFileName: baseFileName,
|
|
destinationDir: localSubmissionDir,
|
|
);
|
|
Map<String, dynamic> ftpDataResult = {'success': true, 'statuses': []};
|
|
if (dataZip != null) {
|
|
ftpDataResult = await _submissionFtpService.submit(
|
|
moduleName: moduleName,
|
|
fileToUpload: dataZip,
|
|
remotePath: '/${p.basename(dataZip.path)}',
|
|
);
|
|
}
|
|
|
|
// Create and upload image ZIP
|
|
final imageZip = await _zippingService.createImageZip(
|
|
imageFiles: imageFiles.values.toList(),
|
|
baseFileName: baseFileName,
|
|
destinationDir: localSubmissionDir,
|
|
);
|
|
Map<String, dynamic> ftpImageResult = {'success': true, 'statuses': []};
|
|
if (imageZip != null) {
|
|
ftpImageResult = await _submissionFtpService.submit(
|
|
moduleName: moduleName,
|
|
fileToUpload: imageZip,
|
|
remotePath: '/${p.basename(imageZip.path)}',
|
|
);
|
|
}
|
|
|
|
return {
|
|
'statuses': <Map<String, dynamic>>[
|
|
...(ftpDataResult['statuses'] as List? ?? []),
|
|
...(ftpImageResult['statuses'] as List? ?? []),
|
|
],
|
|
};
|
|
}
|
|
|
|
/// Saves or updates the local log file and saves a record to the central DB log.
|
|
Future<void> _logAndSave({
|
|
required TarballSamplingData data,
|
|
required String status,
|
|
required String message,
|
|
required List<Map<String, dynamic>> apiResults,
|
|
required List<Map<String, dynamic>> ftpStatuses,
|
|
required String serverName,
|
|
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
|
|
}) async {
|
|
data.submissionStatus = status;
|
|
data.submissionMessage = message;
|
|
final baseFileName = _generateBaseFileName(data); // Use helper
|
|
|
|
// Prepare log data map including file paths
|
|
Map<String, dynamic> logMapData = data.toDbJson();
|
|
final imageFileMap = data.toImageFiles();
|
|
imageFileMap.forEach((key, file) {
|
|
logMapData[key] = file?.path; // Store path or null
|
|
});
|
|
// Add submission metadata
|
|
logMapData['submissionStatus'] = status;
|
|
logMapData['submissionMessage'] = message;
|
|
// --- 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['api_status'] = jsonEncode(apiResults.where((r) => r.isNotEmpty).toList());
|
|
logMapData['ftp_status'] = jsonEncode(ftpStatuses);
|
|
|
|
// Update or save the specific local JSON log file
|
|
if (logDirectory != null && logDirectory.isNotEmpty) {
|
|
logMapData['logDirectory'] = logDirectory; // Ensure logDirectory path is in the map
|
|
await _localStorageService.updateTarballLog(logMapData); // Use specific update method
|
|
} else {
|
|
await _localStorageService.saveTarballSamplingData(data, serverName: serverName); // Use specific save method
|
|
}
|
|
|
|
// Save a record to the central SQLite submission log table
|
|
final logData = {
|
|
// --- START: MODIFIED TO USE TIMESTAMP ID ---
|
|
'submission_id': data.reportId ?? baseFileName, // This is the timestamp ID
|
|
'module': 'marine', // Correct module
|
|
'type': 'Tarball', // Correct type
|
|
'status': status,
|
|
'message': message,
|
|
'report_id': apiRecordId, // This is the server DB ID
|
|
// --- END: MODIFIED TO USE TIMESTAMP ID ---
|
|
'created_at': DateTime.now().toIso8601String(),
|
|
'form_data': jsonEncode(logMapData), // Log the comprehensive map with paths
|
|
'image_data': jsonEncode(finalImageFiles.values.map((f) => f.path).toList()),
|
|
'server_name': serverName,
|
|
'api_status': jsonEncode(apiResults),
|
|
'ftp_status': jsonEncode(ftpStatuses),
|
|
};
|
|
try {
|
|
await _dbHelper.saveSubmissionLog(logData);
|
|
} catch (e) {
|
|
debugPrint("Error saving Tarball submission log to DB: $e");
|
|
}
|
|
}
|
|
|
|
/// Handles sending or queuing the Telegram alert for Tarball submissions.
|
|
Future<void> _handleTarballSuccessAlert(TarballSamplingData data, List<Map<String, dynamic>>? appSettings, {required bool isDataOnly, bool isSessionExpired = false}) async {
|
|
|
|
// --- START: Logic moved from data model ---
|
|
String generateTarballTelegramAlertMessage(TarballSamplingData data, {required bool isDataOnly}) {
|
|
final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)";
|
|
final stationName = data.selectedStation?['tbl_station_name'] ?? 'N/A';
|
|
final stationCode = data.selectedStation?['tbl_station_code'] ?? 'N/A';
|
|
final classification = data.selectedClassification?['classification_name'] ?? data.classificationId?.toString() ?? 'N/A';
|
|
// --- START MODIFICATION: Add time ---
|
|
final submissionDate = data.samplingDate ?? DateFormat('yyyy-MM-dd').format(DateTime.now());
|
|
final submissionTime = data.samplingTime ?? DateFormat('HH:mm:ss').format(DateTime.now());
|
|
// --- END MODIFICATION ---
|
|
|
|
final buffer = StringBuffer()
|
|
..writeln('✅ *Tarball Sample $submissionType Submitted:*')
|
|
..writeln()
|
|
..writeln('*Station Name & Code:* $stationName ($stationCode)')
|
|
// --- START MODIFICATION: Add time ---
|
|
..writeln('*Date & Time of Submission:* $submissionDate $submissionTime')
|
|
// --- END MODIFICATION ---
|
|
..writeln('*Submitted by User:* ${data.firstSampler}') // Use firstSampler from data model
|
|
..writeln('*Status of Submission:* Successful');
|
|
|
|
// --- START MODIFICATION: Add Tarball Detected Alert ---
|
|
final bool isTarballDetected = classification.isNotEmpty && classification != 'N/A' && classification.toLowerCase() != 'none';
|
|
if (isTarballDetected) {
|
|
buffer
|
|
..writeln()
|
|
..writeln('🔔 *Tarball Detected:*')
|
|
..writeln('*Classification:* $classification');
|
|
} else {
|
|
// If not detected, still show classification
|
|
buffer.writeln('*Classification:* $classification');
|
|
}
|
|
// --- END MODIFICATION ---
|
|
|
|
final distanceKm = data.distanceDifference ?? 0; // Use distanceDifference from data model
|
|
final distanceMeters = (distanceKm * 1000).toStringAsFixed(0);
|
|
final distanceRemarks = data.distanceDifferenceRemarks ?? '';
|
|
|
|
// Check distance > 50m OR if remarks were provided anyway
|
|
if (distanceKm * 1000 > 50 || (distanceRemarks.isNotEmpty)) {
|
|
buffer
|
|
..writeln()
|
|
..writeln('🔔 *Distance Alert:*')
|
|
// --- START MODIFICATION: Add KM ---
|
|
..writeln('*Distance from station:* $distanceMeters meters (${distanceKm.toStringAsFixed(3)} KM)');
|
|
// --- END MODIFICATION ---
|
|
|
|
if (distanceRemarks.isNotEmpty) {
|
|
buffer.writeln('*Remarks for distance:* $distanceRemarks');
|
|
}
|
|
}
|
|
|
|
return buffer.toString();
|
|
}
|
|
// --- END: Logic moved from data model ---
|
|
|
|
try {
|
|
final message = generateTarballTelegramAlertMessage(data, isDataOnly: isDataOnly); // Call local function
|
|
final alertKey = 'marine_tarball'; // Correct key
|
|
|
|
if (isSessionExpired) {
|
|
debugPrint("Session is expired; queuing Telegram alert directly for $alertKey.");
|
|
await _telegramService.queueMessage(alertKey, message, appSettings);
|
|
} else {
|
|
final bool wasSent = await _telegramService.sendAlertImmediately(alertKey, message, appSettings);
|
|
if (!wasSent) {
|
|
await _telegramService.queueMessage(alertKey, message, appSettings);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Failed to handle Tarball Telegram alert: $e");
|
|
}
|
|
}
|
|
} |