From 033391b770e0b0a2f64fa7ecdbe502e2d07f51dc Mon Sep 17 00:00:00 2001 From: ALim Aidrus Date: Sun, 23 Nov 2025 22:07:48 +0800 Subject: [PATCH] add telegram alert for marine report module --- lib/main.dart | 7 +- ...ine_manual_equipment_maintenance_data.dart | 34 ++- lib/models/marine_manual_npe_report_data.dart | 27 +- ...e_manual_pre_departure_checklist_data.dart | 46 +++- .../marine_manual_sonde_calibration_data.dart | 40 ++- .../reports/npe_report_from_in_situ.dart | 19 +- .../reports/npe_report_from_tarball.dart | 19 +- .../reports/npe_report_new_location.dart | 20 +- .../submission_preferences_settings.dart | 1 + ..._manual_equipment_maintenance_service.dart | 241 +++++++++++++----- .../marine_manual_pre_departure_service.dart | 225 ++++++++++++---- ...rine_manual_sonde_calibration_service.dart | 234 ++++++++++++----- lib/services/marine_npe_report_service.dart | 224 ++++++++-------- lib/services/user_preferences_service.dart | 28 +- 14 files changed, 821 insertions(+), 344 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 0144655..4c6e2d5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -132,9 +132,10 @@ void main() async { // --- START: Instantiate Marine Report Services --- final MarineNpeReportService marineNpeService = MarineNpeReportService(telegramService); - final MarineManualPreDepartureService marinePreDepartureService = MarineManualPreDepartureService(apiService); - final MarineManualSondeCalibrationService marineSondeCalibrationService = MarineManualSondeCalibrationService(apiService); - final MarineManualEquipmentMaintenanceService marineEquipmentMaintenanceService = MarineManualEquipmentMaintenanceService(apiService); + // FIX: Added telegramService to constructors below + final MarineManualPreDepartureService marinePreDepartureService = MarineManualPreDepartureService(apiService, telegramService); + final MarineManualSondeCalibrationService marineSondeCalibrationService = MarineManualSondeCalibrationService(apiService, telegramService); + final MarineManualEquipmentMaintenanceService marineEquipmentMaintenanceService = MarineManualEquipmentMaintenanceService(apiService, telegramService); // --- END: Instantiate Marine Report Services --- telegramService.setApiService(apiService); diff --git a/lib/models/marine_manual_equipment_maintenance_data.dart b/lib/models/marine_manual_equipment_maintenance_data.dart index 9d5d5b8..574288d 100644 --- a/lib/models/marine_manual_equipment_maintenance_data.dart +++ b/lib/models/marine_manual_equipment_maintenance_data.dart @@ -4,9 +4,7 @@ import 'dart:convert'; class MarineManualEquipmentMaintenanceData { int? conductedByUserId; - // --- START: ADDED FIELD --- String? conductedByUserName; - // --- END: ADDED FIELD --- String? maintenanceDate; String? lastMaintenanceDate; String? scheduleMaintenance; @@ -30,11 +28,9 @@ class MarineManualEquipmentMaintenanceData { String? vanDornNewSerial; Map> vanDornReplacements = {}; - // --- START: Added Fields --- String? submissionStatus; String? submissionMessage; String? reportId; - // --- END: Added Fields --- // Constructor to initialize maps @@ -76,7 +72,6 @@ class MarineManualEquipmentMaintenanceData { vanDornReplacements[item] = {'Last Date': '', 'New Date': ''}); } - // --- START: ADDED METHOD --- /// Creates a JSON object for offline database storage. Map toDbJson() { return { @@ -104,7 +99,6 @@ class MarineManualEquipmentMaintenanceData { 'reportId': reportId, }; } - // --- END: ADDED METHOD --- // MODIFIED: This method now builds the complex nested structure the PHP controller expects. Map toApiFormData() { @@ -178,4 +172,32 @@ class MarineManualEquipmentMaintenanceData { 'van_dorn_replacements': vanDornReplacementsList, }; } + + // --- START: ADDED TELEGRAM METHOD --- + String generateTelegramAlertMessage() { + final buffer = StringBuffer() + ..writeln('🛠 *Equipment Maintenance Report Submitted*') + ..writeln() + ..writeln('*Conducted By:* ${conductedByUserName ?? "N/A"}') + ..writeln('*Date:* ${maintenanceDate ?? "N/A"}') + ..writeln('*Time:* ${timeStart ?? "-"} to ${timeEnd ?? "-"}') + ..writeln('*Location:* ${location ?? "N/A"}') + ..writeln('*Type:* ${isReplacement ? "Replacement" : "Routine Check"}') + ..writeln('*Schedule:* ${scheduleMaintenance ?? "N/A"}'); + + if (ysiSondeComments != null && ysiSondeComments!.isNotEmpty) { + buffer.writeln('\n*YSI Remarks:* $ysiSondeComments'); + } + + if (vanDornComments != null && vanDornComments!.isNotEmpty) { + buffer.writeln('\n*Van Dorn Remarks:* $vanDornComments'); + } + + buffer + ..writeln() + ..writeln('*Status of Submission:* Successful'); + + return buffer.toString(); + } +// --- END: ADDED TELEGRAM METHOD --- } \ No newline at end of file diff --git a/lib/models/marine_manual_npe_report_data.dart b/lib/models/marine_manual_npe_report_data.dart index ea0389b..7b963ce 100644 --- a/lib/models/marine_manual_npe_report_data.dart +++ b/lib/models/marine_manual_npe_report_data.dart @@ -113,16 +113,29 @@ class MarineManualNpeReportData { add('npe_time', eventTime); add('first_sampler_user_id', firstSamplerUserId); - // add('npe_source_origin', sourceOrigin); // Disabled to prevent SQL error - + // --- FIX START: Correctly map 'station_id' to specific API fields --- if (selectedStation != null) { - add('station_id', selectedStation?['station_id'] ?? selectedStation?['tbl_station_id']); - add('station_code', selectedStation?['man_station_code'] ?? selectedStation?['tbl_station_code']); - add('station_name', selectedStation?['man_station_name'] ?? selectedStation?['tbl_station_name']); + // Check if it is a Manual Station (has 'man_station_code') + if (selectedStation!.containsKey('man_station_code')) { + // Map generic 'station_id' to API's expected 'man_station_id' + add('man_station_id', selectedStation!['station_id'] ?? selectedStation!['man_station_id']); + add('station_code', selectedStation!['man_station_code']); + add('station_name', selectedStation!['man_station_name']); + } + // Check if it is a Tarball Station (has 'tbl_station_code') + else if (selectedStation!.containsKey('tbl_station_code')) { + // Map generic 'station_id' to API's expected 'tbl_station_id' + add('tbl_station_id', selectedStation!['station_id'] ?? selectedStation!['tbl_station_id']); + add('station_code', selectedStation!['tbl_station_code']); + add('station_name', selectedStation!['tbl_station_name']); + } } else { - add('station_name', locationDescription); + // New Location: Use description + add('location_description', locationDescription); add('state_name', stateName); } + // --- FIX END --- + add('latitude', latitude); add('longitude', longitude); add('npe_oxygen_sat', oxygenSaturation); @@ -166,7 +179,6 @@ class MarineManualNpeReportData { String generateTelegramAlertMessage() { String locationDesc; - // --- START: MODIFIED to include Station Code + Name --- if (selectedStation != null) { final code = selectedStation!['man_station_code'] ?? selectedStation!['tbl_station_code'] ?? 'N/A'; final name = selectedStation!['man_station_name'] ?? selectedStation!['tbl_station_name'] ?? 'N/A'; @@ -174,7 +186,6 @@ class MarineManualNpeReportData { } else { locationDesc = locationDescription ?? 'A custom location'; } - // --- END: MODIFIED --- final buffer = StringBuffer() ..writeln('🚨 *Notification of Pollution Event (NPE) Submitted:*') diff --git a/lib/models/marine_manual_pre_departure_checklist_data.dart b/lib/models/marine_manual_pre_departure_checklist_data.dart index 24bb76b..14d8648 100644 --- a/lib/models/marine_manual_pre_departure_checklist_data.dart +++ b/lib/models/marine_manual_pre_departure_checklist_data.dart @@ -6,9 +6,7 @@ class MarineManualPreDepartureChecklistData { String? reporterName; int? reporterUserId; String? submissionDate; - // --- START: ADDED FIELD --- String? location; - // --- END: ADDED FIELD --- // Key: Item description, Value: true if 'Yes', false if 'No' Map checklistItems = {}; @@ -16,22 +14,19 @@ class MarineManualPreDepartureChecklistData { // Key: Item description, Value: Remarks text Map remarks = {}; - // --- START: Added Fields --- String? submissionStatus; String? submissionMessage; String? reportId; - // --- END: Added Fields --- MarineManualPreDepartureChecklistData(); - // --- START: ADDED METHOD --- /// Creates a JSON object for offline database storage. Map toDbJson() { return { 'reporterName': reporterName, 'reporterUserId': reporterUserId, 'submissionDate': submissionDate, - 'location': location, // <-- ADDED + 'location': location, 'checklistItems': checklistItems, 'remarks': remarks, 'submissionStatus': submissionStatus, @@ -39,7 +34,6 @@ class MarineManualPreDepartureChecklistData { 'reportId': reportId, }; } - // --- END: ADDED METHOD --- // MODIFIED: This method now builds the nested array structure the PHP controller expects. Map toApiFormData() { @@ -57,12 +51,42 @@ class MarineManualPreDepartureChecklistData { }); return { - 'reporter_user_id': reporterUserId.toString(), // The controller gets this from auth, but good to send. + 'reporter_user_id': reporterUserId.toString(), 'submission_date': submissionDate, - // Note: 'location' is not sent to the API in this method, - // but it will be saved in the local log via toDbJson(). - // If the API needs it, it must be added here. + // Note: 'location' is not sent to the API in this method, but saved in db.json 'items': itemsList, // Send the formatted list }; } + + // --- START: ADDED TELEGRAM METHOD --- + String generateTelegramAlertMessage() { + int checkedCount = checklistItems.values.where((v) => v == true).length; + int totalCount = checklistItems.length; + + final buffer = StringBuffer() + ..writeln('🚤 *Pre-Departure Checklist Submitted*') + ..writeln() + ..writeln('*Reporter:* ${reporterName ?? "N/A"}') + ..writeln('*Date:* ${submissionDate ?? "N/A"}') + ..writeln('*Location:* ${location ?? "N/A"}') + ..writeln('*Items Checked:* $checkedCount / $totalCount'); + + // Add remarks only if they exist for specific items + final activeRemarks = remarks.entries + .where((e) => e.value.trim().isNotEmpty) + .map((e) => "- ${e.key}: ${e.value}") + .toList(); + + if (activeRemarks.isNotEmpty) { + buffer.writeln('\n*Specific Remarks:*'); + buffer.writeAll(activeRemarks, '\n'); + } + + buffer + ..writeln() + ..writeln('*Status of Submission:* Successful'); + + return buffer.toString(); + } +// --- END: ADDED TELEGRAM METHOD --- } \ No newline at end of file diff --git a/lib/models/marine_manual_sonde_calibration_data.dart b/lib/models/marine_manual_sonde_calibration_data.dart index 471f4b9..dd437d2 100644 --- a/lib/models/marine_manual_sonde_calibration_data.dart +++ b/lib/models/marine_manual_sonde_calibration_data.dart @@ -2,9 +2,7 @@ class MarineManualSondeCalibrationData { int? calibratedByUserId; - // --- START: ADDED FIELD --- String? calibratedByUserName; - // --- END: ADDED FIELD --- // Header fields from PDF String? sondeSerialNumber; @@ -22,7 +20,7 @@ class MarineManualSondeCalibrationData { double? ph10Before; double? ph10After; - // Other parameters (Mv removed per PDF) + // Other parameters double? condBefore; double? condAfter; double? doBefore; @@ -33,16 +31,13 @@ class MarineManualSondeCalibrationData { double? turbidity124After; String? calibrationStatus; - String? remarks; // Matches "COMMENT/OBSERVATION" + String? remarks; - // --- START: Added Fields --- String? submissionStatus; String? submissionMessage; String? reportId; - // --- END: Added Fields --- Map toApiFormData() { - // This flat structure matches MarineSondeCalibrationController.php return { 'calibrated_by_user_id': calibratedByUserId.toString(), 'sonde_serial_number': sondeSerialNumber, @@ -70,12 +65,11 @@ class MarineManualSondeCalibrationData { }; } - // --- START: ADDED toDbJson METHOD --- /// Creates a JSON object for offline database storage. Map toDbJson() { return { 'calibratedByUserId': calibratedByUserId, - 'calibratedByUserName': calibratedByUserName, // <-- ADDED + 'calibratedByUserName': calibratedByUserName, 'sondeSerialNumber': sondeSerialNumber, 'firmwareVersion': firmwareVersion, 'korVersion': korVersion, @@ -103,5 +97,31 @@ class MarineManualSondeCalibrationData { 'reportId': reportId, }; } -// --- END: ADDED toDbJson METHOD --- + + // --- START: ADDED TELEGRAM METHOD --- + String generateTelegramAlertMessage() { + final buffer = StringBuffer() + ..writeln('⚖️ *Sonde Calibration Report Submitted*') + ..writeln() + ..writeln('*Calibrated By:* ${calibratedByUserName ?? "N/A"}') + ..writeln('*Location:* ${location ?? "N/A"}') + ..writeln('*Sonde Serial:* ${sondeSerialNumber ?? "N/A"}') + ..writeln('*Start:* ${startDateTime ?? "N/A"}') + ..writeln('*End:* ${endDateTime ?? "N/A"}'); + + if (calibrationStatus != null && calibrationStatus!.isNotEmpty) { + buffer.writeln('*Result:* $calibrationStatus'); + } + + if (remarks != null && remarks!.isNotEmpty) { + buffer.writeln('\n*Remarks:* $remarks'); + } + + buffer + ..writeln() + ..writeln('*Status of Submission:* Successful'); + + return buffer.toString(); + } +// --- END: ADDED TELEGRAM METHOD --- } \ No newline at end of file diff --git a/lib/screens/marine/manual/reports/npe_report_from_in_situ.dart b/lib/screens/marine/manual/reports/npe_report_from_in_situ.dart index ea6f6d4..a120875 100644 --- a/lib/screens/marine/manual/reports/npe_report_from_in_situ.dart +++ b/lib/screens/marine/manual/reports/npe_report_from_in_situ.dart @@ -368,12 +368,29 @@ class _NPEReportFromInSituState extends State with WidgetsB if (_isPickingImage) return; setState(() => _isPickingImage = true); + // --- FIX START: Extract correct code and name for proper filename generation --- + String stationName = _locationController.text; + String stationCode = 'NA'; + + // Check _npeData first as it is the source of truth for the selected station + if (_npeData.selectedStation != null) { + stationCode = _npeData.selectedStation!['man_station_code'] ?? + _npeData.selectedStation!['tbl_station_code'] ?? + 'NA'; + } + // --- FIX END --- + final watermarkData = InSituSamplingData() ..samplingDate = _eventDateTimeController.text.split(' ')[0] ..samplingTime = _eventDateTimeController.text.split(' ').length > 1 ? _eventDateTimeController.text.split(' ')[1] : '' ..currentLatitude = _latController.text ..currentLongitude = _longController.text - ..selectedStation = {'man_station_name': _locationController.text}; + // --- FIX: Pass BOTH code and name to the image service --- + ..selectedStation = { + 'man_station_name': stationName, + 'man_station_code': stationCode, + 'tbl_station_code': stationCode, // Fallback depending on processor logic + }; final file = await _samplingService.pickAndProcessImage( source, diff --git a/lib/screens/marine/manual/reports/npe_report_from_tarball.dart b/lib/screens/marine/manual/reports/npe_report_from_tarball.dart index 297c0e6..0162e65 100644 --- a/lib/screens/marine/manual/reports/npe_report_from_tarball.dart +++ b/lib/screens/marine/manual/reports/npe_report_from_tarball.dart @@ -249,12 +249,29 @@ class _NPEReportFromTarballState extends State with Widget if (_isPickingImage) return; setState(() => _isPickingImage = true); + // --- FIX START: Extract correct code and name for proper filename generation --- + String stationName = _locationController.text; + String stationCode = 'NA'; + + // Check _npeData first as it is the source of truth for the selected station + if (_npeData.selectedStation != null) { + stationCode = _npeData.selectedStation!['man_station_code'] ?? + _npeData.selectedStation!['tbl_station_code'] ?? + 'NA'; + } + // --- FIX END --- + final watermarkData = InSituSamplingData() ..samplingDate = _eventDateTimeController.text.split(' ')[0] ..samplingTime = _eventDateTimeController.text.split(' ').length > 1 ? _eventDateTimeController.text.split(' ')[1] : '' ..currentLatitude = _latController.text ..currentLongitude = _longController.text - ..selectedStation = {'man_station_name': _locationController.text}; + // --- FIX: Pass BOTH code and name to the image service --- + ..selectedStation = { + 'man_station_name': stationName, + 'man_station_code': stationCode, + 'tbl_station_code': stationCode, // Fallback depending on processor logic + }; final file = await _samplingService.pickAndProcessImage( source, diff --git a/lib/screens/marine/manual/reports/npe_report_new_location.dart b/lib/screens/marine/manual/reports/npe_report_new_location.dart index 1451d58..388c479 100644 --- a/lib/screens/marine/manual/reports/npe_report_new_location.dart +++ b/lib/screens/marine/manual/reports/npe_report_new_location.dart @@ -234,12 +234,28 @@ class _NPEReportNewLocationState extends State with Widget if (_isPickingImage) return; setState(() => _isPickingImage = true); + // --- FIX START: Sanitize Location Description for Filename --- + String rawLocation = _locationController.text.trim(); + String filenamePrefix; + + if (rawLocation.isNotEmpty) { + // Replace spaces with underscores for the filename + filenamePrefix = rawLocation.replaceAll(' ', '_'); + } else { + // Fallback if the user hasn't typed anything yet + filenamePrefix = 'NEW_LOCATION'; + } + // --- FIX END --- + final watermarkData = InSituSamplingData() ..samplingDate = _eventDateTimeController.text.split(' ')[0] ..samplingTime = _eventDateTimeController.text.split(' ').length > 1 ? _eventDateTimeController.text.split(' ')[1] : '' ..currentLatitude = _latController.text ..currentLongitude = _longController.text - ..selectedStation = {'man_station_name': _locationController.text}; + ..selectedStation = { + 'man_station_name': rawLocation, // Keep spaces for the Watermark text + 'man_station_code': filenamePrefix, // Use underscores for the Filename + }; final file = await _samplingService.pickAndProcessImage( source, @@ -618,7 +634,7 @@ class _NPEReportNewLocationState extends State with Widget Widget _buildNPEImagePicker({ required String title, - File? imageFile, + imageFile, required VoidCallback onClear, required int imageNumber, required TextEditingController remarkController, // ADDED diff --git a/lib/screens/settings/submission_preferences_settings.dart b/lib/screens/settings/submission_preferences_settings.dart index 179a6a3..bbffbc1 100644 --- a/lib/screens/settings/submission_preferences_settings.dart +++ b/lib/screens/settings/submission_preferences_settings.dart @@ -37,6 +37,7 @@ class _SubmissionPreferencesSettingsScreenState {'key': 'marine_tarball', 'name': 'Marine Tarball'}, {'key': 'marine_in_situ', 'name': 'Marine In-Situ'}, {'key': 'marine_investigative', 'name': 'Marine Investigative'}, + {'key': 'marine_report', 'name': 'Marine Report'}, {'key': 'river_in_situ', 'name': 'River In-Situ'}, {'key': 'river_triennial', 'name': 'River Triennial'}, {'key': 'river_investigative', 'name': 'River Investigative'}, diff --git a/lib/services/marine_manual_equipment_maintenance_service.dart b/lib/services/marine_manual_equipment_maintenance_service.dart index 8896548..3fbe311 100644 --- a/lib/services/marine_manual_equipment_maintenance_service.dart +++ b/lib/services/marine_manual_equipment_maintenance_service.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:path/path.dart' as p; import '../auth_provider.dart'; import '../models/marine_manual_equipment_maintenance_data.dart'; @@ -14,40 +15,52 @@ 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/retry_service.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/zipping_service.dart'; +import 'user_preferences_service.dart'; // ADDED +import 'telegram_service.dart'; // --- ADDED IMPORT --- import 'base_api_service.dart'; // Import for SessionExpiredException class MarineManualEquipmentMaintenanceService { // Use the new generic submission service final SubmissionApiService _submissionApiService = SubmissionApiService(); + final SubmissionFtpService _submissionFtpService = SubmissionFtpService(); + final ZippingService _zippingService = ZippingService(); final LocalStorageService _localStorageService = LocalStorageService(); final ServerConfigService _serverConfigService = ServerConfigService(); + final UserPreferencesService _userPreferencesService = UserPreferencesService(); // ADDED final DatabaseHelper _dbHelper = DatabaseHelper(); final RetryService _retryService = RetryService(); + final TelegramService _telegramService; // --- ADDED FIELD --- // Keep ApiService for getPreviousMaintenanceLogs final ApiService _apiService; - MarineManualEquipmentMaintenanceService(this._apiService); - // *** START: Renamed this method *** + // --- MODIFIED CONSTRUCTOR --- + MarineManualEquipmentMaintenanceService(this._apiService, this._telegramService); + + /// Fetches all Maintenance logs stored locally on the device. + Future>> getLocalMaintenanceLogs() async { + return await _localStorageService.getAllEquipmentMaintenanceLogs(); + } + /// Main submission method with online/offline branching logic Future> submitMaintenanceReport({ - // *** END: Renamed this method *** required MarineManualEquipmentMaintenanceData data, required AuthProvider authProvider, - List>? appSettings, // Added for consistency - BuildContext? context, // Added for consistency + List>? appSettings, + BuildContext? context, String? logDirectory, }) async { - const String moduleName = 'marine_equipment_maintenance'; + // Unified module name for preferences + const String moduleName = 'marine_report'; - // --- START: ADDED LINE --- // Populate the user name from the AuthProvider data.conductedByUserName = authProvider.profileData?['first_name'] as String?; - // --- END: ADDED LINE --- final connectivityResult = await Connectivity().checkConnectivity(); - bool isOnline = connectivityResult != ConnectivityResult.none; + bool isOnline = !connectivityResult.contains(ConnectivityResult.none); bool isOfflineSession = authProvider.isLoggedIn && (authProvider.profileData?['token'] ?.startsWith("offline-session-") ?? @@ -93,56 +106,107 @@ class MarineManualEquipmentMaintenanceService { (await _serverConfigService.getActiveApiConfig())?['config_name'] as String? ?? 'Default'; - Map apiResult; + bool anyApiSuccess = false; + Map apiResult = {}; - try { - apiResult = await _submissionApiService.submitPost( - moduleName: moduleName, - endpoint: 'marine/maintenance', // Endpoint from marine_api_service.dart - body: data.toApiFormData(), - ); - } on SessionExpiredException { - final bool reloginSuccess = await authProvider.attemptSilentRelogin(); - if (reloginSuccess) { + // 1. API Submission + final pref = await _userPreferencesService.getModulePreference(moduleName); + bool isApiEnabled = pref?['is_api_enabled'] ?? true; + + if (isApiEnabled) { + try { apiResult = await _submissionApiService.submitPost( moduleName: moduleName, - endpoint: 'marine/maintenance', + endpoint: 'marine/maintenance', // Endpoint from marine_api_service.dart body: data.toApiFormData(), ); - } else { + + if (apiResult['success'] == false && (apiResult['message'] as String?)?.contains('Unauthorized') == true) { + // Handle silent relogin + if (await authProvider.attemptSilentRelogin()) { + apiResult = await _submissionApiService.submitPost( + moduleName: moduleName, + endpoint: 'marine/maintenance', + body: data.toApiFormData(), + ); + } + } + + if (apiResult['success'] == true) { + anyApiSuccess = true; + data.reportId = apiResult['data']?['maintenance_id']?.toString(); + } + } on SocketException catch (e) { apiResult = { 'success': false, - 'message': 'Session expired. Please log in again.' + 'message': "API submission failed with network error: $e" + }; + // Queue API manually if failed + await _retryService.addApiToQueue(endpoint: 'marine/maintenance', method: 'POST', body: data.toApiFormData()); + } on TimeoutException catch (e) { + apiResult = { + 'success': false, + 'message': "API submission timed out: $e" + }; + await _retryService.addApiToQueue(endpoint: 'marine/maintenance', method: 'POST', body: data.toApiFormData()); + } catch (e) { + apiResult = { + 'success': false, + 'message': 'An unexpected error occurred: $e' }; } - } on SocketException catch (e) { - apiResult = { - 'success': false, - 'message': "API submission failed with network error: $e" - }; - // submission_api_service will queue this failure - } on TimeoutException catch (e) { - apiResult = { - 'success': false, - 'message': "API submission timed out: $e" - }; - // submission_api_service will queue this failure - } catch (e) { - apiResult = { - 'success': false, - 'message': 'An unexpected error occurred: $e' - }; + } else { + anyApiSuccess = true; // Treat as success if disabled by user } - // Log the final result - final bool overallSuccess = apiResult['success'] == true; - final String finalMessage = - apiResult['message'] ?? (overallSuccess ? 'Submission successful.' : 'Submission failed.'); - final String finalStatus = overallSuccess ? 'S4' : 'L1'; // S4 = API Success + // 2. FTP Submission (Data Zip Only - No Images for Maintenance) + Map ftpResults = {'statuses': []}; + bool anyFtpSuccess = false; - if (overallSuccess) { - // Assuming the API returns an ID. Adjust 'maintenance_id' if needed. - data.reportId = apiResult['data']?['maintenance_id']?.toString(); + bool isFtpEnabled = pref?['is_ftp_enabled'] ?? true; + // Check status to avoid duplicate uploads (L4 = API Fail, FTP Success; S4 = Both Success) + bool previousFtpSuccess = data.submissionStatus == 'L4' || data.submissionStatus == 'S4'; + // Check active FTPs + final enabledFtpConfigs = await _userPreferencesService.getEnabledFtpConfigsForModule(moduleName); + + if (!isFtpEnabled) { + ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'FTP disabled by user.', 'success': true}]}; + anyFtpSuccess = true; + } else if (previousFtpSuccess) { + debugPrint("FTP submission skipped: Already successful."); + ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'Already successful.', 'success': true}]}; + anyFtpSuccess = true; + } else if (enabledFtpConfigs.isEmpty) { + debugPrint("FTP submission skipped: No active FTP configurations found for $moduleName."); + ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'No active FTP servers.', 'success': true}]}; + anyFtpSuccess = true; + } else { + try { + ftpResults = await _generateAndUploadFtpFiles(data, serverName, moduleName); + anyFtpSuccess = !(ftpResults['statuses'] as List).any((status) => status['success'] == false && status['status'] != 'Not Configured'); + } catch (e) { + debugPrint("FTP submission error: $e"); + anyFtpSuccess = false; + } + } + + // 3. Determine Final Status + final bool overallSuccess = anyApiSuccess || anyFtpSuccess; + String finalMessage; + String finalStatus; + + if (anyApiSuccess && anyFtpSuccess) { + finalMessage = 'Maintenance Report submitted successfully to all destinations.'; + finalStatus = 'S4'; + } else if (anyApiSuccess && !anyFtpSuccess) { + finalMessage = 'Maintenance Report sent to API, but FTP upload failed.'; + finalStatus = 'S3'; + } else if (!anyApiSuccess && anyFtpSuccess) { + finalMessage = 'API submission failed, but file sent to FTP.'; + finalStatus = 'L4'; + } else { + finalMessage = apiResult['message'] ?? 'All submission attempts failed.'; + finalStatus = 'L1'; } await _logAndSave( @@ -150,11 +214,18 @@ class MarineManualEquipmentMaintenanceService { status: finalStatus, message: finalMessage, apiResult: apiResult, + ftpStatuses: ftpResults['statuses'], serverName: serverName, logDirectory: logDirectory, ); - return apiResult; + // --- START: ADDED TELEGRAM ALERT --- + if (overallSuccess) { + _handleSuccessAlert(data, authProvider); + } + // --- END: ADDED TELEGRAM ALERT --- + + return {'success': overallSuccess, 'message': finalMessage}; } /// Handles saving the submission to local storage and queuing for retry. @@ -181,12 +252,13 @@ class MarineManualEquipmentMaintenanceService { status: 'Error', message: message, apiResult: {}, + ftpStatuses: [], serverName: serverName); return {'success': false, 'message': message}; } await _retryService.queueTask( - type: 'equipment_maintenance_submission', // New task type + type: 'equipment_maintenance_submission', payload: { 'module': moduleName, 'localLogPath': localLogPath, @@ -199,12 +271,40 @@ class MarineManualEquipmentMaintenanceService { return {'success': true, 'message': successMessage}; } + /// Generates zip files and uploads them via FTP. + Future> _generateAndUploadFtpFiles(MarineManualEquipmentMaintenanceData data, String serverName, String moduleName) async { + final timestamp = data.maintenanceDate ?? DateTime.now().toIso8601String(); + final baseFileName = 'maintenance_$timestamp'; + + final Directory? logDirectory = await _localStorageService.getLogDirectory(serverName: serverName, module: 'marine', subModule: 'marine_equipment_maintenance',); + final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, baseFileName)) : null; + if (localSubmissionDir != null && !await localSubmissionDir.exists()) await localSubmissionDir.create(recursive: true); + + final dataZip = await _zippingService.createDataZip( + jsonDataMap: {'db.json': jsonEncode(data.toDbJson())}, + baseFileName: baseFileName, + destinationDir: localSubmissionDir, + ); + + Map ftpDataResult = {'success': true, 'statuses': []}; + if (dataZip != null) { + ftpDataResult = await _submissionFtpService.submit( + moduleName: moduleName, + fileToUpload: dataZip, // Added ! to ensure non-nullable + remotePath: '/${p.basename(dataZip.path)}' + ); + } + + return {'statuses': ftpDataResult['statuses'] ?? []}; + } + /// Logs the submission to the local file system and the central SQL database. Future _logAndSave({ required MarineManualEquipmentMaintenanceData data, required String status, required String message, required Map apiResult, + required List> ftpStatuses, required String serverName, String? logDirectory, }) async { @@ -213,24 +313,20 @@ class MarineManualEquipmentMaintenanceService { final fileTimestamp = data.maintenanceDate ?? DateTime.now().toIso8601String(); + // Use the new toDbJson() method to get ALL data for logging + final Map logDataMap = data.toDbJson(); + // Add submission-specific metadata + logDataMap['api_status'] = jsonEncode(apiResult); + logDataMap['ftp_status'] = jsonEncode(ftpStatuses); + logDataMap['serverConfigName'] = serverName; + + if (logDirectory != null) { // This is an update to an existing log file - // --- START: MODIFIED BLOCK --- - final Map updatedLogData = data.toDbJson(); - // Add metadata - updatedLogData['submissionStatus'] = status; - updatedLogData['submissionMessage'] = message; - updatedLogData['logDirectory'] = logDirectory; - updatedLogData['serverConfigName'] = serverName; - updatedLogData['api_status'] = jsonEncode(apiResult); - // All other fields (ysiSondeChecks, etc.) are now in toDbJson() - // --- END: MODIFIED BLOCK --- - - // This method is added to LocalStorageService - await _localStorageService.updateEquipmentMaintenanceLog(updatedLogData); + logDataMap['logDirectory'] = logDirectory; + await _localStorageService.updateEquipmentMaintenanceLog(logDataMap); } else { // This is a new log - // This method is added to LocalStorageService await _localStorageService.saveEquipmentMaintenanceData(data, serverName: serverName); } @@ -242,19 +338,16 @@ class MarineManualEquipmentMaintenanceService { 'message': data.submissionMessage, 'report_id': data.reportId, 'created_at': DateTime.now().toIso8601String(), - // --- START: MODIFIED LINE --- 'form_data': jsonEncode(data.toDbJson()), // Log the full DbJson - // --- END: MODIFIED LINE --- 'image_data': null, // No images 'server_name': serverName, 'api_status': jsonEncode(apiResult), - 'ftp_status': null, // No FTP + 'ftp_status': jsonEncode(ftpStatuses), }; await _dbHelper.saveSubmissionLog(logData); } /// Fetches previous maintenance logs to populate the form - /// THIS METHOD IS UNCHANGED as it's a simple GET request. Future> getPreviousMaintenanceLogs({ required AuthProvider authProvider, }) async { @@ -275,4 +368,18 @@ class MarineManualEquipmentMaintenanceService { return {'success': false, 'message': 'An unexpected error occurred: $e'}; } } + + // --- START: NEW TELEGRAM ALERT METHOD --- + Future _handleSuccessAlert(MarineManualEquipmentMaintenanceData data, AuthProvider authProvider) async { + try { + final message = data.generateTelegramAlertMessage(); + // Using 'marine_npe_report' ID/module config as requested + if (!await _telegramService.sendAlertImmediately('marine_npe_report', message, authProvider.appSettings)) { + await _telegramService.queueMessage('marine_npe_report', message, authProvider.appSettings); + } + } catch (e) { + debugPrint("Telegram Alert Error (Maintenance): $e"); + } + } +// --- END: NEW TELEGRAM ALERT METHOD --- } \ No newline at end of file diff --git a/lib/services/marine_manual_pre_departure_service.dart b/lib/services/marine_manual_pre_departure_service.dart index ba09ba3..f485353 100644 --- a/lib/services/marine_manual_pre_departure_service.dart +++ b/lib/services/marine_manual_pre_departure_service.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:path/path.dart' as p; import '../auth_provider.dart'; import '../models/marine_manual_pre_departure_checklist_data.dart'; @@ -14,44 +15,52 @@ 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/retry_service.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/zipping_service.dart'; +import 'user_preferences_service.dart'; // ADDED +import 'telegram_service.dart'; // --- ADDED IMPORT --- import 'base_api_service.dart'; // Import for SessionExpiredException class MarineManualPreDepartureService { // Use the new generic submission service final SubmissionApiService _submissionApiService = SubmissionApiService(); + final SubmissionFtpService _submissionFtpService = SubmissionFtpService(); + final ZippingService _zippingService = ZippingService(); final LocalStorageService _localStorageService = LocalStorageService(); final ServerConfigService _serverConfigService = ServerConfigService(); + final UserPreferencesService _userPreferencesService = UserPreferencesService(); // ADDED final DatabaseHelper _dbHelper = DatabaseHelper(); final RetryService _retryService = RetryService(); + final TelegramService _telegramService; // --- ADDED FIELD --- // The ApiService is kept only if other non-submission methods need it. - // For this refactor, we'll remove it from the constructor. - // final ApiService _apiService; - // MarineManualPreDepartureService(this._apiService); - MarineManualPreDepartureService(ApiService apiService); // Keep constructor signature for main.dart + // --- MODIFIED CONSTRUCTOR --- + MarineManualPreDepartureService(ApiService apiService, this._telegramService); + + /// Fetches all Checklist logs stored locally on the device. + Future>> getLocalChecklistLogs() async { + return await _localStorageService.getAllPreDepartureLogs(); + } /// Main submission method with online/offline branching logic Future> submitChecklist({ required MarineManualPreDepartureChecklistData data, required AuthProvider authProvider, - List>? appSettings, // Added for consistency - BuildContext? context, // Added for consistency + List>? appSettings, + BuildContext? context, String? logDirectory, }) async { - const String moduleName = 'marine_pre_departure'; + // Unified module name for preferences + const String moduleName = 'marine_report'; - // --- START: ADDED LINE --- // Populate the user name from the AuthProvider data.reporterName = authProvider.profileData?['first_name'] as String?; - // --- END: ADDED LINE --- final connectivityResult = await Connectivity().checkConnectivity(); - bool isOnline = connectivityResult != ConnectivityResult.none; + bool isOnline = !connectivityResult.contains(ConnectivityResult.none); bool isOfflineSession = authProvider.isLoggedIn && - (authProvider.profileData?['token'] - ?.startsWith("offline-session-") ?? - false); + (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false); if (isOnline && isOfflineSession) { debugPrint( @@ -93,56 +102,108 @@ class MarineManualPreDepartureService { (await _serverConfigService.getActiveApiConfig())?['config_name'] as String? ?? 'Default'; - Map apiResult; + bool anyApiSuccess = false; + Map apiResult = {}; - try { - apiResult = await _submissionApiService.submitPost( - moduleName: moduleName, - endpoint: 'marine/checklist', // Endpoint from marine_api_service.dart - body: data.toApiFormData(), - ); - } on SessionExpiredException { - final bool reloginSuccess = await authProvider.attemptSilentRelogin(); - if (reloginSuccess) { + // 1. API Submission + // Check if API is enabled in preferences + final pref = await _userPreferencesService.getModulePreference(moduleName); + bool isApiEnabled = pref?['is_api_enabled'] ?? true; + + if (isApiEnabled) { + try { apiResult = await _submissionApiService.submitPost( moduleName: moduleName, - endpoint: 'marine/checklist', + endpoint: 'marine/checklist', // Endpoint from marine_api_service.dart body: data.toApiFormData(), ); - } else { + + if (apiResult['success'] == false && (apiResult['message'] as String?)?.contains('Unauthorized') == true) { + // Handle silent relogin + if (await authProvider.attemptSilentRelogin()) { + apiResult = await _submissionApiService.submitPost( + moduleName: moduleName, + endpoint: 'marine/checklist', + body: data.toApiFormData(), + ); + } + } + + if (apiResult['success'] == true) { + anyApiSuccess = true; + data.reportId = apiResult['data']?['checklist_id']?.toString(); + } + } on SocketException catch (e) { apiResult = { 'success': false, - 'message': 'Session expired. Please log in again.' + 'message': "API submission failed with network error: $e" + }; + // Queue API manually + await _retryService.addApiToQueue(endpoint: 'marine/checklist', method: 'POST', body: data.toApiFormData()); + } on TimeoutException catch (e) { + apiResult = { + 'success': false, + 'message': "API submission timed out: $e" + }; + await _retryService.addApiToQueue(endpoint: 'marine/checklist', method: 'POST', body: data.toApiFormData()); + } catch (e) { + apiResult = { + 'success': false, + 'message': 'An unexpected error occurred: $e' }; } - } on SocketException catch (e) { - apiResult = { - 'success': false, - 'message': "API submission failed with network error: $e" - }; - // submission_api_service will queue this failure - } on TimeoutException catch (e) { - apiResult = { - 'success': false, - 'message': "API submission timed out: $e" - }; - // submission_api_service will queue this failure - } catch (e) { - apiResult = { - 'success': false, - 'message': 'An unexpected error occurred: $e' - }; + } else { + anyApiSuccess = true; // Treated as success if disabled by user } - // Log the final result - final bool overallSuccess = apiResult['success'] == true; - final String finalMessage = - apiResult['message'] ?? (overallSuccess ? 'Submission successful.' : 'Submission failed.'); - final String finalStatus = overallSuccess ? 'S4' : 'L1'; // S4 = API Success + // 2. FTP Submission (Data Zip Only - No Images for Checklist) + Map ftpResults = {'statuses': []}; + bool anyFtpSuccess = false; - if (overallSuccess) { - // Assuming the API returns an ID. Adjust 'checklist_id' if needed. - data.reportId = apiResult['data']?['checklist_id']?.toString(); + bool isFtpEnabled = pref?['is_ftp_enabled'] ?? true; + // Check if this record was already successfully sent to FTP (L4 or S4 status) + bool previousFtpSuccess = data.submissionStatus == 'L4' || data.submissionStatus == 'S4'; + // Check if there are any active FTP configs for this module + final enabledFtpConfigs = await _userPreferencesService.getEnabledFtpConfigsForModule(moduleName); + + if (!isFtpEnabled) { + ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'FTP disabled by user.', 'success': true}]}; + anyFtpSuccess = true; + } else if (previousFtpSuccess) { + debugPrint("FTP submission skipped: Already successful in previous attempt."); + ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'Already successful.', 'success': true}]}; + anyFtpSuccess = true; + } else if (enabledFtpConfigs.isEmpty) { + debugPrint("FTP submission skipped: No active FTP configurations found for $moduleName."); + ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'No active FTP servers.', 'success': true}]}; + anyFtpSuccess = true; // Treated as success to avoid indefinite L1 state + } else { + try { + ftpResults = await _generateAndUploadFtpFiles(data, serverName, moduleName); + anyFtpSuccess = !(ftpResults['statuses'] as List).any((status) => status['success'] == false && status['status'] != 'Not Configured'); + } catch (e) { + debugPrint("FTP submission error: $e"); + anyFtpSuccess = false; + } + } + + // 3. Determine Final Status + final bool overallSuccess = anyApiSuccess || anyFtpSuccess; + String finalMessage; + String finalStatus; + + if (anyApiSuccess && anyFtpSuccess) { + finalMessage = 'Checklist submitted successfully to all destinations.'; + finalStatus = 'S4'; + } else if (anyApiSuccess && !anyFtpSuccess) { + finalMessage = 'Checklist sent to API, but FTP upload failed.'; + finalStatus = 'S3'; + } else if (!anyApiSuccess && anyFtpSuccess) { + finalMessage = 'API submission failed, but file sent to FTP.'; + finalStatus = 'L4'; + } else { + finalMessage = apiResult['message'] ?? 'All submission attempts failed.'; + finalStatus = 'L1'; } await _logAndSave( @@ -150,11 +211,18 @@ class MarineManualPreDepartureService { status: finalStatus, message: finalMessage, apiResult: apiResult, + ftpStatuses: ftpResults['statuses'], serverName: serverName, logDirectory: logDirectory, ); - return apiResult; + // --- START: ADDED TELEGRAM ALERT --- + if (overallSuccess) { + _handleSuccessAlert(data, authProvider); + } + // --- END: ADDED TELEGRAM ALERT --- + + return {'success': overallSuccess, 'message': finalMessage}; } /// Handles saving the submission to local storage and queuing for retry. @@ -181,6 +249,7 @@ class MarineManualPreDepartureService { status: 'Error', message: message, apiResult: {}, + ftpStatuses: [], serverName: serverName); return {'success': false, 'message': message}; } @@ -199,12 +268,47 @@ class MarineManualPreDepartureService { return {'success': true, 'message': successMessage}; } + /// Generates zip files and uploads them via FTP. + Future> _generateAndUploadFtpFiles(MarineManualPreDepartureChecklistData data, String serverName, String moduleName) async { + final timestamp = data.submissionDate ?? DateTime.now().toIso8601String(); + final baseFileName = 'checklist_$timestamp'; + + final Directory? logDirectory = await _localStorageService.getLogDirectory( + serverName: serverName, + module: 'marine', + subModule: 'marine_pre_departure', + ); + + final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, baseFileName)) : null; + if (localSubmissionDir != null && !await localSubmissionDir.exists()) { + await localSubmissionDir.create(recursive: true); + } + + final dataZip = await _zippingService.createDataZip( + jsonDataMap: {'db.json': jsonEncode(data.toDbJson())}, + baseFileName: baseFileName, + destinationDir: localSubmissionDir, + ); + + Map ftpDataResult = {'success': true, 'statuses': []}; + if (dataZip != null) { + ftpDataResult = await _submissionFtpService.submit( + moduleName: moduleName, + fileToUpload: dataZip, // Added ! to ensure non-nullable if needed, but flow guarantees it's checked or handled + remotePath: '/${p.basename(dataZip.path)}' + ); + } + + return {'statuses': ftpDataResult['statuses'] ?? []}; + } + /// Logs the submission to the local file system and the central SQL database. Future _logAndSave({ required MarineManualPreDepartureChecklistData data, required String status, required String message, required Map apiResult, + required List> ftpStatuses, required String serverName, String? logDirectory, }) async { @@ -223,6 +327,7 @@ class MarineManualPreDepartureService { updatedLogData['logDirectory'] = logDirectory; updatedLogData['serverConfigName'] = serverName; updatedLogData['api_status'] = jsonEncode(apiResult); + updatedLogData['ftp_status'] = jsonEncode(ftpStatuses); // All other fields are now in toDbJson() // --- END: MODIFIED BLOCK --- @@ -248,8 +353,22 @@ class MarineManualPreDepartureService { 'image_data': null, // No images 'server_name': serverName, 'api_status': jsonEncode(apiResult), - 'ftp_status': null, // No FTP + 'ftp_status': jsonEncode(ftpStatuses), }; await _dbHelper.saveSubmissionLog(logData); } + + // --- START: NEW TELEGRAM ALERT METHOD --- + Future _handleSuccessAlert(MarineManualPreDepartureChecklistData data, AuthProvider authProvider) async { + try { + final message = data.generateTelegramAlertMessage(); + // Using 'marine_npe_report' ID/module config as requested + if (!await _telegramService.sendAlertImmediately('marine_npe_report', message, authProvider.appSettings)) { + await _telegramService.queueMessage('marine_npe_report', message, authProvider.appSettings); + } + } catch (e) { + debugPrint("Telegram Alert Error (Checklist): $e"); + } + } +// --- END: NEW TELEGRAM ALERT METHOD --- } \ No newline at end of file diff --git a/lib/services/marine_manual_sonde_calibration_service.dart b/lib/services/marine_manual_sonde_calibration_service.dart index 2fbf44a..a7c70d0 100644 --- a/lib/services/marine_manual_sonde_calibration_service.dart +++ b/lib/services/marine_manual_sonde_calibration_service.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:path/path.dart' as p; import '../auth_provider.dart'; import '../models/marine_manual_sonde_calibration_data.dart'; @@ -14,44 +15,51 @@ 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/retry_service.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/zipping_service.dart'; +import 'user_preferences_service.dart'; // ADDED +import 'telegram_service.dart'; // --- ADDED IMPORT --- import 'base_api_service.dart'; // Import for SessionExpiredException class MarineManualSondeCalibrationService { - // Use the new generic submission service final SubmissionApiService _submissionApiService = SubmissionApiService(); + final SubmissionFtpService _submissionFtpService = SubmissionFtpService(); + final ZippingService _zippingService = ZippingService(); final LocalStorageService _localStorageService = LocalStorageService(); final ServerConfigService _serverConfigService = ServerConfigService(); + final UserPreferencesService _userPreferencesService = UserPreferencesService(); // ADDED final DatabaseHelper _dbHelper = DatabaseHelper(); final RetryService _retryService = RetryService(); + final TelegramService _telegramService; // --- ADDED FIELD --- // The ApiService is kept only if other non-submission methods need it. - // For this refactor, we'll remove it from the constructor. - // final ApiService _apiService; - // MarineManualSondeCalibrationService(this._apiService); - MarineManualSondeCalibrationService(ApiService apiService); // Keep constructor signature for main.dart + // --- MODIFIED CONSTRUCTOR --- + MarineManualSondeCalibrationService(ApiService apiService, this._telegramService); + + /// Fetches all Calibration logs stored locally on the device. + Future>> getLocalCalibrationLogs() async { + return await _localStorageService.getAllSondeCalibrationLogs(); + } /// Main submission method with online/offline branching logic Future> submitCalibration({ required MarineManualSondeCalibrationData data, required AuthProvider authProvider, - List>? appSettings, // Added for consistency - BuildContext? context, // Added for consistency + List>? appSettings, + BuildContext? context, String? logDirectory, }) async { - const String moduleName = 'marine_sonde_calibration'; + // Unified module name for preferences + const String moduleName = 'marine_report'; - // --- START: ADDED LINE --- // Populate the user name from the AuthProvider data.calibratedByUserName = authProvider.profileData?['first_name'] as String?; - // --- END: ADDED LINE --- final connectivityResult = await Connectivity().checkConnectivity(); - bool isOnline = connectivityResult != ConnectivityResult.none; + bool isOnline = !connectivityResult.contains(ConnectivityResult.none); bool isOfflineSession = authProvider.isLoggedIn && - (authProvider.profileData?['token'] - ?.startsWith("offline-session-") ?? - false); + (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false); if (isOnline && isOfflineSession) { debugPrint( @@ -93,56 +101,108 @@ class MarineManualSondeCalibrationService { (await _serverConfigService.getActiveApiConfig())?['config_name'] as String? ?? 'Default'; - Map apiResult; + bool anyApiSuccess = false; + Map apiResult = {}; - try { - apiResult = await _submissionApiService.submitPost( - moduleName: moduleName, - endpoint: 'marine/calibration', // Endpoint from marine_api_service.dart - body: data.toApiFormData(), - ); - } on SessionExpiredException { - final bool reloginSuccess = await authProvider.attemptSilentRelogin(); - if (reloginSuccess) { + // 1. API Submission + // Check if API is enabled in preferences + final pref = await _userPreferencesService.getModulePreference(moduleName); + bool isApiEnabled = pref?['is_api_enabled'] ?? true; + + if (isApiEnabled) { + try { apiResult = await _submissionApiService.submitPost( moduleName: moduleName, - endpoint: 'marine/calibration', + endpoint: 'marine/calibration', // Endpoint from marine_api_service.dart body: data.toApiFormData(), ); - } else { + + if (apiResult['success'] == false && (apiResult['message'] as String?)?.contains('Unauthorized') == true) { + // Handle silent relogin + if (await authProvider.attemptSilentRelogin()) { + apiResult = await _submissionApiService.submitPost( + moduleName: moduleName, + endpoint: 'marine/calibration', + body: data.toApiFormData(), + ); + } + } + + if (apiResult['success'] == true) { + anyApiSuccess = true; + data.reportId = apiResult['data']?['calibration_id']?.toString(); + } + } on SocketException catch (e) { apiResult = { 'success': false, - 'message': 'Session expired. Please log in again.' + 'message': "API submission failed with network error: $e" + }; + // Queue API manually + await _retryService.addApiToQueue(endpoint: 'marine/calibration', method: 'POST', body: data.toApiFormData()); + } on TimeoutException catch (e) { + apiResult = { + 'success': false, + 'message': "API submission timed out: $e" + }; + await _retryService.addApiToQueue(endpoint: 'marine/calibration', method: 'POST', body: data.toApiFormData()); + } catch (e) { + apiResult = { + 'success': false, + 'message': 'An unexpected error occurred: $e' }; } - } on SocketException catch (e) { - apiResult = { - 'success': false, - 'message': "API submission failed with network error: $e" - }; - // submission_api_service will queue this failure - } on TimeoutException catch (e) { - apiResult = { - 'success': false, - 'message': "API submission timed out: $e" - }; - // submission_api_service will queue this failure - } catch (e) { - apiResult = { - 'success': false, - 'message': 'An unexpected error occurred: $e' - }; + } else { + anyApiSuccess = true; // Treated as success if disabled by user } - // Log the final result - final bool overallSuccess = apiResult['success'] == true; - final String finalMessage = - apiResult['message'] ?? (overallSuccess ? 'Submission successful.' : 'Submission failed.'); - final String finalStatus = overallSuccess ? 'S4' : 'L1'; // S4 = API Success + // 2. FTP Submission (Data Zip Only - No Images for Calibration) + Map ftpResults = {'statuses': []}; + bool anyFtpSuccess = false; - if (overallSuccess) { - // Assuming the API returns an ID. Adjust 'calibration_id' if needed. - data.reportId = apiResult['data']?['calibration_id']?.toString(); + bool isFtpEnabled = pref?['is_ftp_enabled'] ?? true; + // Check status to avoid duplicate uploads (L4 = API Fail, FTP Success; S4 = Both Success) + bool previousFtpSuccess = data.submissionStatus == 'L4' || data.submissionStatus == 'S4'; + // Check active FTPs + final enabledFtpConfigs = await _userPreferencesService.getEnabledFtpConfigsForModule(moduleName); + + if (!isFtpEnabled) { + ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'FTP disabled by user.', 'success': true}]}; + anyFtpSuccess = true; + } else if (previousFtpSuccess) { + debugPrint("FTP submission skipped: Already successful in previous attempt."); + ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'Already successful.', 'success': true}]}; + anyFtpSuccess = true; + } else if (enabledFtpConfigs.isEmpty) { + debugPrint("FTP submission skipped: No active FTP configurations found for $moduleName."); + ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'No active FTP servers.', 'success': true}]}; + anyFtpSuccess = true; + } else { + try { + ftpResults = await _generateAndUploadFtpFiles(data, serverName, moduleName); + anyFtpSuccess = !(ftpResults['statuses'] as List).any((status) => status['success'] == false && status['status'] != 'Not Configured'); + } catch (e) { + debugPrint("FTP submission error: $e"); + anyFtpSuccess = false; + } + } + + // 3. Determine Final Status + final bool overallSuccess = anyApiSuccess || anyFtpSuccess; + String finalMessage; + String finalStatus; + + if (anyApiSuccess && anyFtpSuccess) { + finalMessage = 'Calibration submitted successfully to all destinations.'; + finalStatus = 'S4'; + } else if (anyApiSuccess && !anyFtpSuccess) { + finalMessage = 'Calibration sent to API, but FTP upload failed.'; + finalStatus = 'S3'; + } else if (!anyApiSuccess && anyFtpSuccess) { + finalMessage = 'API submission failed, but file sent to FTP.'; + finalStatus = 'L4'; + } else { + finalMessage = apiResult['message'] ?? 'All submission attempts failed.'; + finalStatus = 'L1'; } await _logAndSave( @@ -150,11 +210,18 @@ class MarineManualSondeCalibrationService { status: finalStatus, message: finalMessage, apiResult: apiResult, + ftpStatuses: ftpResults['statuses'], serverName: serverName, logDirectory: logDirectory, ); - return apiResult; + // --- START: ADDED TELEGRAM ALERT --- + if (overallSuccess) { + _handleSuccessAlert(data, authProvider); + } + // --- END: ADDED TELEGRAM ALERT --- + + return {'success': overallSuccess, 'message': finalMessage}; } /// Handles saving the submission to local storage and queuing for retry. @@ -181,6 +248,7 @@ class MarineManualSondeCalibrationService { status: 'Error', message: message, apiResult: {}, + ftpStatuses: [], serverName: serverName); return {'success': false, 'message': message}; } @@ -199,12 +267,47 @@ class MarineManualSondeCalibrationService { return {'success': true, 'message': successMessage}; } + /// Generates zip files and uploads them via FTP. + Future> _generateAndUploadFtpFiles(MarineManualSondeCalibrationData data, String serverName, String moduleName) async { + final fileTimestamp = data.startDateTime?.replaceAll(':', '-').replaceAll(' ', '_') ?? DateTime.now().toIso8601String(); + final baseFileName = 'calibration_${data.sondeSerialNumber}_$fileTimestamp'; + + final Directory? logDirectory = await _localStorageService.getLogDirectory( + serverName: serverName, + module: 'marine', + subModule: 'marine_sonde_calibration', + ); + + final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, baseFileName)) : null; + if (localSubmissionDir != null && !await localSubmissionDir.exists()) { + await localSubmissionDir.create(recursive: true); + } + + final dataZip = await _zippingService.createDataZip( + jsonDataMap: {'db.json': jsonEncode(data.toDbJson())}, + baseFileName: baseFileName, + destinationDir: localSubmissionDir, + ); + + Map ftpDataResult = {'success': true, 'statuses': []}; + if (dataZip != null) { + ftpDataResult = await _submissionFtpService.submit( + moduleName: moduleName, + fileToUpload: dataZip, // Added ! to ensure non-nullable + remotePath: '/${p.basename(dataZip.path)}' + ); + } + + return {'statuses': ftpDataResult['statuses'] ?? []}; + } + /// Logs the submission to the local file system and the central SQL database. Future _logAndSave({ required MarineManualSondeCalibrationData data, required String status, required String message, required Map apiResult, + required List> ftpStatuses, required String serverName, String? logDirectory, }) async { @@ -213,14 +316,12 @@ class MarineManualSondeCalibrationService { final fileTimestamp = data.startDateTime?.replaceAll(':', '-').replaceAll(' ', '_') ?? DateTime.now().toIso8601String(); - // --- START: MODIFIED BLOCK --- // Use the new toDbJson() method to get ALL data for logging final Map logDataMap = data.toDbJson(); // Add submission-specific metadata logDataMap['api_status'] = jsonEncode(apiResult); + logDataMap['ftp_status'] = jsonEncode(ftpStatuses); logDataMap['serverConfigName'] = serverName; - // --- END: MODIFIED BLOCK --- - if (logDirectory != null) { // This is an update to an existing log file @@ -228,7 +329,6 @@ class MarineManualSondeCalibrationService { await _localStorageService.updateSondeCalibrationLog(logDataMap); } else { // This is a new log - // Pass the complete data object, which now includes the user name await _localStorageService.saveSondeCalibrationData(data, serverName: serverName); } @@ -240,12 +340,28 @@ class MarineManualSondeCalibrationService { 'message': data.submissionMessage, 'report_id': data.reportId, 'created_at': DateTime.now().toIso8601String(), - 'form_data': jsonEncode(data.toDbJson()), // <-- Use toDbJson here + // --- START: MODIFIED LINE --- + 'form_data': jsonEncode(data.toDbJson()), // Log the full DbJson + // --- END: MODIFIED LINE --- 'image_data': null, // No images 'server_name': serverName, 'api_status': jsonEncode(apiResult), - 'ftp_status': null, // No FTP + 'ftp_status': jsonEncode(ftpStatuses), }; await _dbHelper.saveSubmissionLog(logData); } + + // --- START: NEW TELEGRAM ALERT METHOD --- + Future _handleSuccessAlert(MarineManualSondeCalibrationData data, AuthProvider authProvider) async { + try { + final message = data.generateTelegramAlertMessage(); + // Using 'marine_npe_report' ID/module config as requested + if (!await _telegramService.sendAlertImmediately('marine_npe_report', message, authProvider.appSettings)) { + await _telegramService.queueMessage('marine_npe_report', message, authProvider.appSettings); + } + } catch (e) { + debugPrint("Telegram Alert Error (Calibration): $e"); + } + } +// --- END: NEW TELEGRAM ALERT METHOD --- } \ No newline at end of file diff --git a/lib/services/marine_npe_report_service.dart b/lib/services/marine_npe_report_service.dart index 207c608..8af37c4 100644 --- a/lib/services/marine_npe_report_service.dart +++ b/lib/services/marine_npe_report_service.dart @@ -1,4 +1,4 @@ -// lib/services/marine_tarball_sampling_service.dart +// lib/services/marine_npe_report_service.dart import 'dart:async'; import 'dart:io'; @@ -16,9 +16,8 @@ import 'submission_api_service.dart'; import 'submission_ftp_service.dart'; import 'telegram_service.dart'; import 'retry_service.dart'; -import 'api_service.dart'; import 'package:environment_monitoring_app/services/database_helper.dart'; - +import 'user_preferences_service.dart'; // ADDED class MarineNpeReportService { final SubmissionApiService _submissionApiService = SubmissionApiService(); @@ -26,35 +25,34 @@ class MarineNpeReportService { final ZippingService _zippingService = ZippingService(); final LocalStorageService _localStorageService = LocalStorageService(); final ServerConfigService _serverConfigService = ServerConfigService(); + final UserPreferencesService _userPreferencesService = UserPreferencesService(); // ADDED final DatabaseHelper _dbHelper = DatabaseHelper(); final RetryService _retryService = RetryService(); final TelegramService _telegramService; MarineNpeReportService(this._telegramService); + Future>> getLocalNpeLogs() async { + return await _localStorageService.getAllNpeLogs(); + } + Future> submitNpeReport({ required MarineManualNpeReportData data, required AuthProvider authProvider, String? logDirectory, }) async { - const String moduleName = 'marine_npe_report'; + const String moduleName = 'marine_report'; final connectivityResult = await Connectivity().checkConnectivity(); - bool isOnline = connectivityResult != ConnectivityResult.none; + bool isOnline = !connectivityResult.contains(ConnectivityResult.none); bool isOfflineSession = authProvider.isLoggedIn && (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false); if (isOnline && isOfflineSession) { - debugPrint("NPE submission online during offline session. Attempting auto-relogin..."); final bool transitionSuccess = await authProvider.checkAndTransitionToOnlineSession(); - if (transitionSuccess) { - isOfflineSession = false; - } else { - isOnline = false; - } + if (transitionSuccess) isOfflineSession = false; else isOnline = false; } if (isOnline && !isOfflineSession) { - debugPrint("Proceeding with direct ONLINE NPE submission..."); return await _performNpeOnlineSubmission( data: data, moduleName: moduleName, @@ -62,7 +60,6 @@ class MarineNpeReportService { logDirectory: logDirectory, ); } else { - debugPrint("Proceeding with OFFLINE NPE queuing mechanism..."); return await _performNpeOfflineQueuing( data: data, moduleName: moduleName, @@ -85,92 +82,92 @@ class MarineNpeReportService { Map apiDataResult = {}; Map apiImageResult = {}; - try { - // --- MODIFIED: Use the new endpoint path for data --- - apiDataResult = await _submissionApiService.submitPost( - moduleName: moduleName, - endpoint: 'marine/npe/report', // <-- Updated endpoint - body: data.toApiFormData(), - ); + // 1. API Submission + // Check if API is enabled in preferences + final pref = await _userPreferencesService.getModulePreference(moduleName); + bool isApiEnabled = pref?['is_api_enabled'] ?? true; - if (apiDataResult['success'] == false && - (apiDataResult['message'] as String?)?.contains('Unauthorized') == true) { - final bool reloginSuccess = await authProvider.attemptSilentRelogin(); - if (reloginSuccess) { - apiDataResult = await _submissionApiService.submitPost( - moduleName: moduleName, - endpoint: 'marine/npe/report', // <-- Updated endpoint - body: data.toApiFormData(), - ); + if (isApiEnabled) { + try { + apiDataResult = await _submissionApiService.submitPost( + moduleName: moduleName, + endpoint: 'marine/npe/report', + body: data.toApiFormData(), + ); + + if (apiDataResult['success'] == false && (apiDataResult['message'] as String?)?.contains('Unauthorized') == true) { + if (await authProvider.attemptSilentRelogin()) { + apiDataResult = await _submissionApiService.submitPost( + moduleName: moduleName, + endpoint: 'marine/npe/report', + body: data.toApiFormData(), + ); + } } - } - if (apiDataResult['success'] == true) { - anyApiSuccess = true; - data.reportId = apiDataResult['data']?['npe_id']?.toString(); + if (apiDataResult['success'] == true) { + anyApiSuccess = true; + data.reportId = apiDataResult['data']?['npe_id']?.toString(); - if (data.reportId != null) { - if (finalImageFiles.isNotEmpty) { - // --- MODIFIED: Use the new endpoint path for images --- + if (data.reportId != null && finalImageFiles.isNotEmpty) { apiImageResult = await _submissionApiService.submitMultipart( moduleName: moduleName, - endpoint: 'marine/npe/images', // <-- Updated endpoint + endpoint: 'marine/npe/images', fields: {'npe_id': data.reportId!}, files: finalImageFiles, ); if (apiImageResult['success'] != true) anyApiSuccess = false; } - } else { - anyApiSuccess = false; - apiDataResult['message'] = 'API Error: Submission succeeded but did not return a record ID.'; } + } on SocketException catch (e) { + anyApiSuccess = false; + apiDataResult = {'success': false, 'message': "API Network Error: $e"}; + await _retryService.addApiToQueue(endpoint: 'marine/npe/report', method: 'POST', body: data.toApiFormData()); + } on TimeoutException catch (e) { + anyApiSuccess = false; + apiDataResult = {'success': false, 'message': "API Timeout: $e"}; + await _retryService.addApiToQueue(endpoint: 'marine/npe/report', method: 'POST', body: data.toApiFormData()); } - } on SocketException catch (e) { - anyApiSuccess = false; - apiDataResult = {'success': false, 'message': "API submission failed with network error: $e"}; - // --- MODIFIED: Update queue with new endpoints --- - await _retryService.addApiToQueue(endpoint: 'marine/npe/report', method: 'POST', body: data.toApiFormData()); - if (finalImageFiles.isNotEmpty && data.reportId != null) { - await _retryService.addApiToQueue(endpoint: 'marine/npe/images', method: 'POST_MULTIPART', fields: {'npe_id': data.reportId!}, files: finalImageFiles); - } - } on TimeoutException catch (e) { - anyApiSuccess = false; - apiDataResult = {'success': false, 'message': "API submission timed out: $e"}; - // --- MODIFIED: Update queue with new endpoint --- - await _retryService.addApiToQueue(endpoint: 'marine/npe/report', method: 'POST', body: data.toApiFormData()); + } else { + anyApiSuccess = true; // Treated as success if disabled by user } + // 2. FTP Submission Map ftpResults = {'statuses': []}; bool anyFtpSuccess = false; - try { - ftpResults = await _generateAndUploadFtpFiles(data, finalImageFiles, serverName, moduleName); - anyFtpSuccess = !(ftpResults['statuses'] as List).any((status) => status['success'] == false && status['status'] != 'Not Configured'); - } on SocketException catch (e) { - debugPrint("FTP submission failed with network error: $e"); - anyFtpSuccess = false; - } on TimeoutException catch (e) { - debugPrint("FTP submission timed out: $e"); - anyFtpSuccess = false; - } - final bool overallSuccess = anyApiSuccess || anyFtpSuccess; - String finalMessage; - String finalStatus; + bool isFtpEnabled = pref?['is_ftp_enabled'] ?? true; + // Check if this record was already successfully sent to FTP (L4 or S4 status) + bool previousFtpSuccess = data.submissionStatus == 'L4' || data.submissionStatus == 'S4'; + // Check if there are any active FTP configs for this module + final enabledFtpConfigs = await _userPreferencesService.getEnabledFtpConfigsForModule(moduleName); - if (anyApiSuccess && anyFtpSuccess) { - finalMessage = 'NPE Report submitted successfully to all destinations.'; - finalStatus = 'S4'; - } else if (anyApiSuccess && !anyFtpSuccess) { - finalMessage = 'NPE Report sent to API, but some FTP uploads failed and were queued.'; - finalStatus = 'S3'; - } else if (!anyApiSuccess && anyFtpSuccess) { - finalMessage = 'API submission for NPE Report failed and was queued, but files sent to FTP.'; - finalStatus = 'L4'; + if (!isFtpEnabled) { + ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'FTP disabled by user.', 'success': true}]}; + anyFtpSuccess = true; + } else if (previousFtpSuccess) { + debugPrint("FTP submission skipped: Already successful in previous attempt."); + ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'Already successful.', 'success': true}]}; + anyFtpSuccess = true; + } else if (enabledFtpConfigs.isEmpty) { + debugPrint("FTP submission skipped: No active FTP configurations found for $moduleName."); + ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'No active FTP servers.', 'success': true}]}; + anyFtpSuccess = true; // Treated as success to avoid indefinite L1 state } else { - finalMessage = 'All NPE Report submission attempts failed and have been queued for retry.'; - finalStatus = 'L1'; + 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("FTP Error: $e"); + anyFtpSuccess = false; + } } + // 3. Determine Final Status + final bool overallSuccess = anyApiSuccess || anyFtpSuccess; + String finalStatus = (anyApiSuccess && anyFtpSuccess) ? 'S4' : (anyApiSuccess ? 'S3' : (anyFtpSuccess ? 'L4' : 'L1')); + String finalMessage = overallSuccess ? 'Submission successful.' : 'Submission failed/queued.'; + await _logAndSave( data: data, status: finalStatus, @@ -182,9 +179,7 @@ class MarineNpeReportService { logDirectory: logDirectory, ); - if (overallSuccess) { - _handleNpeSuccessAlert(data, authProvider); - } + if (overallSuccess) _handleNpeSuccessAlert(data, authProvider); return {'success': overallSuccess, 'message': finalMessage, 'reportId': data.reportId}; } @@ -197,56 +192,54 @@ class MarineNpeReportService { final serverName = serverConfig?['config_name'] as String? ?? 'Default'; data.submissionStatus = 'L1'; - data.submissionMessage = 'NPE Report queued due to being offline.'; + data.submissionMessage = 'Queued (Offline)'; final String? localLogPath = await _localStorageService.saveNpeReportData(data, serverName: serverName); - if (localLogPath == null) { - const message = "Failed to save NPE report to local device storage."; - await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}); - return {'success': false, 'message': message}; - } + if (localLogPath == null) return {'success': false, 'message': "Failed to save locally."}; await _retryService.queueTask( type: 'npe_submission', - payload: { - 'module': moduleName, - 'localLogPath': localLogPath, - 'serverConfig': serverConfig, - }, + payload: {'module': moduleName, 'localLogPath': localLogPath, 'serverConfig': serverConfig}, ); - const successMessage = "No internet connection. NPE Report has been saved and queued for upload."; - return {'success': true, 'message': successMessage}; + return {'success': true, 'message': "Saved locally and queued for upload."}; } Future> _generateAndUploadFtpFiles(MarineManualNpeReportData data, Map imageFiles, String serverName, String moduleName) async { - final stationCode = data.selectedStation?['man_station_code'] ?? data.selectedStation?['tbl_station_code'] ?? 'CUSTOM_LOC'; + // --- FIX START: Logic to determine correct Station Code or Sanitized New Location --- + String stationCode; + + if (data.selectedStation != null) { + // If it's an existing station (Manual or Tarball) + stationCode = data.selectedStation?['man_station_code'] ?? + data.selectedStation?['tbl_station_code'] ?? + 'NA'; + } else { + // If it's a New Location, use the description and replace spaces with underscores + String rawDesc = data.locationDescription?.trim() ?? 'NEW_LOCATION'; + if (rawDesc.isEmpty) rawDesc = 'NEW_LOCATION'; + stationCode = rawDesc.replaceAll(' ', '_'); + } + // --- FIX END --- + final fileTimestamp = "${data.eventDate}_${data.eventTime}".replaceAll(':', '-').replaceAll(' ', '_'); final baseFileName = '${stationCode}_${fileTimestamp}_NPE'; - final Directory? logDirectory = await _localStorageService.getLogDirectory( - serverName: serverName, - module: 'marine', - subModule: 'marine_npe_report', - ); + final Directory? logDirectory = await _localStorageService.getLogDirectory(serverName: serverName, module: 'marine', subModule: 'marine_npe_report'); final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, data.reportId ?? baseFileName)) : null; - if (localSubmissionDir != null && !await localSubmissionDir.exists()) { - await localSubmissionDir.create(recursive: true); - } + + if (localSubmissionDir != null && !await localSubmissionDir.exists()) await localSubmissionDir.create(recursive: true); final dataZip = await _zippingService.createDataZip( jsonDataMap: {'db.json': jsonEncode(data.toDbJson())}, baseFileName: baseFileName, destinationDir: localSubmissionDir, ); + Map ftpDataResult = {'success': true, 'statuses': []}; if (dataZip != null) { - ftpDataResult = await _submissionFtpService.submit( - moduleName: moduleName, - fileToUpload: dataZip, - remotePath: '/${p.basename(dataZip.path)}', - ); + ftpDataResult = await _submissionFtpService.submit(moduleName: moduleName, fileToUpload: dataZip, remotePath: '/${p.basename(dataZip.path)}'); } final imageZip = await _zippingService.createImageZip( @@ -256,11 +249,7 @@ class MarineNpeReportService { ); Map ftpImageResult = {'success': true, 'statuses': []}; if (imageZip != null) { - ftpImageResult = await _submissionFtpService.submit( - moduleName: moduleName, - fileToUpload: imageZip, - remotePath: '/${p.basename(imageZip.path)}', - ); + ftpImageResult = await _submissionFtpService.submit(moduleName: moduleName, fileToUpload: imageZip, remotePath: '/${p.basename(imageZip.path)}'); } return { @@ -308,8 +297,8 @@ class MarineNpeReportService { 'submission_id': data.reportId ?? fileTimestamp, 'module': 'marine', 'type': 'NPE', - 'status': data.submissionStatus, - 'message': data.submissionMessage, + 'status': status, + 'message': message, 'report_id': data.reportId, 'created_at': DateTime.now().toIso8601String(), 'form_data': jsonEncode(data.toDbJson()), @@ -324,12 +313,9 @@ class MarineNpeReportService { Future _handleNpeSuccessAlert(MarineManualNpeReportData data, AuthProvider authProvider) async { try { final message = data.generateTelegramAlertMessage(); - final bool wasSent = await _telegramService.sendAlertImmediately('marine_npe_report', message, authProvider.appSettings); - if (!wasSent) { + if (!await _telegramService.sendAlertImmediately('marine_npe_report', message, authProvider.appSettings)) { await _telegramService.queueMessage('marine_npe_report', message, authProvider.appSettings); } - } catch (e) { - debugPrint("Failed to handle NPE Telegram alert: $e"); - } + } catch (e) { debugPrint("Telegram Alert Error: $e"); } } } \ No newline at end of file diff --git a/lib/services/user_preferences_service.dart b/lib/services/user_preferences_service.dart index b0efd05..8d59a94 100644 --- a/lib/services/user_preferences_service.dart +++ b/lib/services/user_preferences_service.dart @@ -17,6 +17,7 @@ class UserPreferencesService { {'key': 'marine_tarball', 'name': 'Marine Tarball'}, {'key': 'marine_in_situ', 'name': 'Marine In-Situ'}, {'key': 'marine_investigative', 'name': 'Marine Investigative'}, + {'key': 'marine_report', 'name': 'Marine Report'}, {'key': 'river_in_situ', 'name': 'River In-Situ'}, {'key': 'river_triennial', 'name': 'River Triennial'}, {'key': 'river_investigative', 'name': 'River Investigative'}, @@ -52,9 +53,21 @@ class UserPreferencesService { ); // 2. Determine default API links - // This is correct: Tick any API server marked as 'is_active' by default. final defaultApiLinks = allApiConfigs.map((config) { - bool isEnabled = (config['is_active'] == 1 || config['is_active'] == true); + bool isActive = (config['is_active'] == 1 || config['is_active'] == true); + bool isPstwHq = (config['config_name'] == 'PSTW_HQ'); + + bool isEnabled; + + // --- MODIFIED: Special logic for Marine Report --- + // For marine_report, ONLY tick PSTW_HQ by default. Ignore other active APIs. + if (moduleKey == 'marine_report') { + isEnabled = isPstwHq; + } else { + // For other modules, tick if Active OR PSTW_HQ + isEnabled = isActive || isPstwHq; + } + return {...config, 'is_enabled': isEnabled}; }).toList(); @@ -149,8 +162,15 @@ class UserPreferencesService { isEnabled = matchingLink['is_enabled'] as bool? ?? false; } else { // No preference saved for this config. Apply default logic. - // (This handles newly synced configs automatically) - isEnabled = (config['is_active'] == 1 || config['is_active'] == true); + bool isActive = (config['is_active'] == 1 || config['is_active'] == true); + bool isPstwHq = (config['config_name'] == 'PSTW_HQ'); + + // --- MODIFIED: Special logic for Marine Report --- + if (moduleName == 'marine_report') { + isEnabled = isPstwHq; + } else { + isEnabled = isActive || isPstwHq; + } } // --- END MODIFICATION ---