From 6c4bc335b8093c8f74dadc9c0eb06b9db4fd596f Mon Sep 17 00:00:00 2001 From: ALim Aidrus Date: Fri, 21 Nov 2025 21:37:32 +0800 Subject: [PATCH] fix issue on marine investigative and npe report --- .../marine_inves_manual_step_2_site_info.dart | 4 +- .../reports/npe_report_from_in_situ.dart | 80 +++++++++---------- .../reports/npe_report_from_tarball.dart | 25 +++++- .../reports/npe_report_new_location.dart | 77 +++++++++++++++++- lib/services/marine_api_service.dart | 9 +-- ...marine_investigative_sampling_service.dart | 9 ++- 6 files changed, 151 insertions(+), 53 deletions(-) diff --git a/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart b/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart index c32f90a..cca70f7 100644 --- a/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart +++ b/lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart @@ -67,7 +67,7 @@ class _MarineInvesManualStep2SiteInfoState extends State setImageCallback(file)); } else if (mounted) { // Corrected snackbar message - _showSnackBar('Image selection failed. Please ensure all photos are taken in landscape (vertical) mode.', isError: true); + _showSnackBar('Image selection failed. Please ensure all photos are taken in landscape (horizontal) mode.', isError: true); } if (mounted) { @@ -150,7 +150,7 @@ class _MarineInvesManualStep2SiteInfoState extends State createState() => _NPEReportFromInSituState(); } -class _NPEReportFromInSituState extends State { +// Modified: Added WidgetsBindingObserver to handle app lifecycle changes (USB permission dialog) +class _NPEReportFromInSituState extends State with WidgetsBindingObserver { final _formKey = GlobalKey(); bool _isLoading = false; bool _isPickingImage = false; - // --- START: MODIFIED STATE VARIABLES --- // Data handling bool? _useRecentSample; // To track Yes/No selection bool _isLoadingRecentSamples = false; // Now triggered on-demand @@ -48,7 +48,6 @@ class _NPEReportFromInSituState extends State { String? _selectedState; String? _selectedCategory; Map? _selectedManualStation; - // --- END: MODIFIED STATE VARIABLES --- // Controllers final _stationIdController = TextEditingController(); @@ -81,6 +80,8 @@ class _NPEReportFromInSituState extends State { @override void initState() { super.initState(); + // Added: Register observer + WidgetsBinding.instance.addObserver(this); _samplingService = Provider.of(context, listen: false); _loadAllStatesFromProvider(); // Load manual stations for "No" path _setDefaultDateTime(); // Set default time for all paths @@ -88,6 +89,8 @@ class _NPEReportFromInSituState extends State { @override void dispose() { + // Added: Remove observer + WidgetsBinding.instance.removeObserver(this); _dataSubscription?.cancel(); _lockoutTimer?.cancel(); if (_samplingService.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) { @@ -117,7 +120,23 @@ class _NPEReportFromInSituState extends State { super.dispose(); } - // --- START: ADDED HELPER METHODS --- + // Added: Handle App Lifecycle changes (specifically for USB permission dialog return) + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + if (mounted) { + final btConnecting = _samplingService.bluetoothConnectionState.value == BluetoothConnectionState.connecting; + final serialConnecting = _samplingService.serialConnectionState.value == SerialConnectionState.connecting; + + // If the UI is still loading or the service thinks it's still connecting (stuck after permission dialog), + // force a disconnect/reset so the user can try again. + if (_isLoading || btConnecting || serialConnecting) { + _disconnectFromAll(); + } + } + } + } + void _setDefaultDateTime() { final now = DateTime.now(); _eventDateTimeController.text = DateFormat('yyyy-MM-dd HH:mm').format(now); @@ -167,19 +186,16 @@ class _NPEReportFromInSituState extends State { _latController.clear(); _longController.clear(); - // --- CHANGE: Clear measurement controllers so they are empty before reading --- _doPercentController.clear(); _doMgLController.clear(); _phController.clear(); _condController.clear(); _turbController.clear(); _tempController.clear(); - // --------------------------------------------------------------------------- _setDefaultDateTime(); // Reset to 'now' }); } - // --- END: ADDED HELPER METHODS --- Future _fetchRecentNearbySamples() async { setState(() => _isLoadingRecentSamples = true); @@ -269,10 +285,7 @@ class _NPEReportFromInSituState extends State { _npeData.latitude = _latController.text; _npeData.longitude = _longController.text; - // selectedStation is already set by either _populateFormFromData or the "No" path dropdown - // _npeData.selectedStation = _selectedRecentSample?.selectedStation; - - _npeData.locationDescription = _locationController.text; // Used by both paths + _npeData.locationDescription = _locationController.text; _npeData.possibleSource = _possibleSourceController.text; _npeData.othersObservationRemark = _othersObservationController.text; _npeData.oxygenSaturation = double.tryParse(_doPercentController.text); @@ -387,7 +400,7 @@ class _NPEReportFromInSituState extends State { if (mounted) setState(() => _isPickingImage = false); } - // --- START: IN-SITU DEVICE METHODS (Unchanged) --- + // --- IN-SITU DEVICE METHODS --- void _updateTextFields(Map readings) { const defaultValue = -999.0; setState(() { @@ -433,7 +446,7 @@ class _NPEReportFromInSituState extends State { } Future _connectToDevice(String type) async { - setState(() => _isLoading = true); // Use main loading indicator + setState(() => _isLoading = true); bool success = false; try { if (type == 'bluetooth') { @@ -463,7 +476,7 @@ class _NPEReportFromInSituState extends State { debugPrint("Connection failed: $e"); if (mounted) _showConnectionFailedDialog(); } finally { - if (mounted) setState(() => _isLoading = false); // Stop main loading indicator + if (mounted) setState(() => _isLoading = false); } return success; } @@ -547,11 +560,10 @@ class _NPEReportFromInSituState extends State { setState(() { _isAutoReading = false; _isLockedOut = false; + _isLoading = false; // Modified: Reset isLoading to unlock buttons }); } } - // --- END: IN-SITU DEVICE METHODS (Unchanged) --- - @override Widget build(BuildContext context) { @@ -565,7 +577,7 @@ class _NPEReportFromInSituState extends State { child: ListView( padding: const EdgeInsets.all(20.0), children: [ - // --- START: SECTION 1 (NEW) --- + // --- SECTION 1 --- _buildSectionTitle("1. Use Recent Sample?"), Row( children: [ @@ -577,9 +589,9 @@ class _NPEReportFromInSituState extends State { onChanged: (value) { setState(() { _useRecentSample = value; - _clearManualStationSelection(); // Clear "No" path data + _clearManualStationSelection(); if (value == true && _recentNearbySamples.isEmpty) { - _fetchRecentNearbySamples(); // Fetch samples on-demand + _fetchRecentNearbySamples(); } }); }, @@ -593,7 +605,7 @@ class _NPEReportFromInSituState extends State { onChanged: (value) { setState(() { _useRecentSample = value; - _clearRecentSampleSelection(); // Clear "Yes" path data + _clearRecentSampleSelection(); }); }, ), @@ -601,10 +613,8 @@ class _NPEReportFromInSituState extends State { ], ), const SizedBox(height: 16), - // --- END: SECTION 1 (NEW) --- - // --- START: SECTION 2 (CONDITIONAL) --- - // "YES" PATH: Select from recent samples + // --- SECTION 2 --- if (_useRecentSample == true) ...[ _buildSectionTitle("2. Select Recent Sample"), if (_isLoadingRecentSamples) @@ -622,7 +632,7 @@ class _NPEReportFromInSituState extends State { if (sample != null) { setState(() { _selectedRecentSample = sample; - _npeData.selectedStation = sample.selectedStation; // CRITICAL: Set station for submission + _npeData.selectedStation = sample.selectedStation; _populateFormFromData(sample); }); } @@ -631,7 +641,6 @@ class _NPEReportFromInSituState extends State { ), ], - // "NO" PATH: Select from manual station list if (_useRecentSample == false) ...[ _buildSectionTitle("2. Select Manual Station"), DropdownSearch( @@ -687,7 +696,7 @@ class _NPEReportFromInSituState extends State { onChanged: (station) { setState(() { _selectedManualStation = station; - _npeData.selectedStation = station; // CRITICAL: Set station for submission + _npeData.selectedStation = station; _stationIdController.text = station?['man_station_code'] ?? ''; _locationController.text = station?['man_station_name'] ?? ''; _latController.text = station?['man_latitude']?.toString() ?? ''; @@ -697,9 +706,8 @@ class _NPEReportFromInSituState extends State { validator: (val) => val == null && _stationsForCategory.isNotEmpty ? "Station is required" : null, ), ], - // --- END: SECTION 2 (CONDITIONAL) --- - // --- START: SHARED SECTIONS (NOW ALWAYS VISIBLE) --- + // --- SHARED SECTIONS --- const SizedBox(height: 24), _buildSectionTitle("Station Information"), _buildTextFormField(controller: _stationIdController, label: "Station ID", readOnly: true), @@ -734,12 +742,10 @@ class _NPEReportFromInSituState extends State { style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15) ), - // Disable button if "Yes/No" hasn't been selected onPressed: _isLoading || _useRecentSample == null ? null : _submitNpeReport, child: _isLoading ? const CircularProgressIndicator(color: Colors.white) : const Text("Submit Report"), ), ), - // --- END: SHARED SECTIONS --- ], ), ), @@ -890,13 +896,10 @@ class _NPEReportFromInSituState extends State { ); } - // --- START: WIDGET BUILDERS FOR IN-SITU (Unchanged) --- Widget _buildInSituSection() { final activeConnection = _getActiveConnectionDetails(); final String? activeType = activeConnection?['type'] as String?; - // For the "No" path, the in-situ fields must be editable. - // For the "Yes" path, they should be read-only as they come from the sample. final bool areFieldsReadOnly = (_useRecentSample == true); return Column( @@ -987,7 +990,7 @@ class _NPEReportFromInSituState extends State { required String label, required String unit, required TextEditingController controller, - bool readOnly = false, // ADDED: readOnly parameter + bool readOnly = false, }) { final bool isMissing = controller.text.isEmpty || controller.text.contains('-999'); final String displayValue = isMissing ? '-.--' : (double.tryParse(controller.text)?.toStringAsFixed(5) ?? '-.--'); @@ -1001,14 +1004,12 @@ class _NPEReportFromInSituState extends State { trailing: SizedBox( width: 120, child: TextFormField( - // --- START: MODIFIED to handle readOnly vs. editable --- controller: readOnly ? null : controller, initialValue: readOnly ? displayValue : null, key: readOnly ? ValueKey(displayValue) : null, - // --- END: MODIFIED --- readOnly: readOnly, textAlign: TextAlign.right, - keyboardType: readOnly ? null : const TextInputType.numberWithOptions(decimal: true), // Allow editing only if NOT readOnly + keyboardType: readOnly ? null : const TextInputType.numberWithOptions(decimal: true), style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: isMissing ? Colors.grey : Theme.of(context).colorScheme.primary, @@ -1016,16 +1017,13 @@ class _NPEReportFromInSituState extends State { decoration: const InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.zero, - isDense: true, // Helps with alignment - // --- CHANGE: Added hint text to display when controller is empty (before start reading) --- + isDense: true, hintText: '-.--', hintStyle: TextStyle(color: Colors.grey), - // ----------------------------------------------------------------------------------------- ), ), ), ), ); } -// --- END: WIDGET BUILDERS FOR IN-SITU --- } \ No newline at end of file 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 bc380ad..297c0e6 100644 --- a/lib/screens/marine/manual/reports/npe_report_from_tarball.dart +++ b/lib/screens/marine/manual/reports/npe_report_from_tarball.dart @@ -26,7 +26,8 @@ class NPEReportFromTarball extends StatefulWidget { State createState() => _NPEReportFromTarballState(); } -class _NPEReportFromTarballState extends State { +// Modified: Added WidgetsBindingObserver to handle app lifecycle changes (USB permission dialog) +class _NPEReportFromTarballState extends State with WidgetsBindingObserver { final _formKey = GlobalKey(); bool _isLoading = false; bool _isPickingImage = false; @@ -70,6 +71,8 @@ class _NPEReportFromTarballState extends State { @override void initState() { super.initState(); + // Added: Register observer + WidgetsBinding.instance.addObserver(this); _samplingService = Provider.of(context, listen: false); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -83,6 +86,8 @@ class _NPEReportFromTarballState extends State { @override void dispose() { + // Added: Remove observer + WidgetsBinding.instance.removeObserver(this); _stationIdController.dispose(); _locationController.dispose(); _eventDateTimeController.dispose(); @@ -106,6 +111,23 @@ class _NPEReportFromTarballState extends State { super.dispose(); } + // Added: Handle App Lifecycle changes (specifically for USB permission dialog return) + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + if (mounted) { + final btConnecting = _samplingService.bluetoothConnectionState.value == BluetoothConnectionState.connecting; + final serialConnecting = _samplingService.serialConnectionState.value == SerialConnectionState.connecting; + + // If the UI is still loading or the service thinks it's still connecting (stuck after permission dialog), + // force a disconnect/reset so the user can try again. + if (_isLoading || btConnecting || serialConnecting) { + _disconnectFromAll(); + } + } + } + } + void _setDefaultDateTime() { final now = DateTime.now(); _eventDateTimeController.text = DateFormat('yyyy-MM-dd HH:mm').format(now); @@ -409,6 +431,7 @@ class _NPEReportFromTarballState extends State { setState(() { _isAutoReading = false; _isLockedOut = false; + _isLoading = false; // Added: Reset isLoading to unlock buttons }); } } 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 d7fcb42..1451d58 100644 --- a/lib/screens/marine/manual/reports/npe_report_new_location.dart +++ b/lib/screens/marine/manual/reports/npe_report_new_location.dart @@ -27,7 +27,8 @@ class NPEReportNewLocation extends StatefulWidget { State createState() => _NPEReportNewLocationState(); } -class _NPEReportNewLocationState extends State { +// Modified: Added WidgetsBindingObserver to handle app lifecycle changes (USB permission dialog) +class _NPEReportNewLocationState extends State with WidgetsBindingObserver { final _formKey = GlobalKey(); bool _isLoading = false; bool _isPickingImage = false; @@ -67,6 +68,8 @@ class _NPEReportNewLocationState extends State { @override void initState() { super.initState(); + // Added: Register observer + WidgetsBinding.instance.addObserver(this); _samplingService = Provider.of(context, listen: false); _setDefaultDateTime(); _loadAllStatesFromProvider(); @@ -74,6 +77,8 @@ class _NPEReportNewLocationState extends State { @override void dispose() { + // Added: Remove observer + WidgetsBinding.instance.removeObserver(this); _dataSubscription?.cancel(); _lockoutTimer?.cancel(); if (_samplingService.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) { @@ -102,6 +107,23 @@ class _NPEReportNewLocationState extends State { super.dispose(); } + // Added: Handle App Lifecycle changes (specifically for USB permission dialog return) + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + if (mounted) { + final btConnecting = _samplingService.bluetoothConnectionState.value == BluetoothConnectionState.connecting; + final serialConnecting = _samplingService.serialConnectionState.value == SerialConnectionState.connecting; + + // If the UI is still loading or the service thinks it's still connecting (stuck after permission dialog), + // force a disconnect/reset so the user can try again. + if (_isLoading || btConnecting || serialConnecting) { + _disconnectFromAll(); + } + } + } + } + void _setDefaultDateTime() { final now = DateTime.now(); _eventDateTimeController.text = DateFormat('yyyy-MM-dd HH:mm').format(now); @@ -243,10 +265,62 @@ class _NPEReportNewLocationState extends State { break; } }); + } else { + await _showImageErrorDialog( + "Image processing failed. Please ensure the photo is taken in landscape mode." + ); } + if (mounted) setState(() => _isPickingImage = false); } + Future _showImageErrorDialog(String message) async { + if (!mounted) return; + return showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Row( + children: [ + Icon(Icons.error_outline, color: Colors.red), + SizedBox(width: 10), + Text('Image Error'), + ], + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(message), + const SizedBox(height: 20), + const Text( + "Please ensure your device is held horizontally:", + style: TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + const Icon( + Icons.stay_current_landscape, + size: 60, + color: Colors.blue, + ), + ], + ), + ), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + Navigator.of(dialogContext).pop(); + }, + ), + ], + ); + }, + ); + } + void _updateTextFields(Map readings) { const defaultValue = -999.0; setState(() { @@ -406,6 +480,7 @@ class _NPEReportNewLocationState extends State { setState(() { _isAutoReading = false; _isLockedOut = false; + _isLoading = false; // Added: Reset isLoading to unlock buttons }); } } diff --git a/lib/services/marine_api_service.dart b/lib/services/marine_api_service.dart index 3cf3ed4..8fcfd1d 100644 --- a/lib/services/marine_api_service.dart +++ b/lib/services/marine_api_service.dart @@ -1,5 +1,3 @@ -// lib/services/marine_api_service.dart - import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; @@ -151,8 +149,9 @@ class MarineApiService { // Pass the station type to the API so it knows which foreign key to check (station_id vs tbl_station_id) final String stationTypeParam = Uri.encodeComponent(stationType); + // *** FIX: Use the correct top-level resource name 'marine-investigative' *** final String endpoint = - 'marine/investigative/records-by-station?station_id=$stationId&date=$dateStr&station_type=$stationTypeParam'; + 'marine-investigative/records-by-station?station_id=$stationId&date=$dateStr&station_type=$stationTypeParam'; debugPrint("MarineApiService: Calling API endpoint: $endpoint"); final response = await _baseService.get(baseUrl, endpoint); @@ -184,10 +183,10 @@ class MarineApiService { 'samplingDate': samplingDate, }; - // Use a new endpoint dedicated to the investigative module + // *** FIX: Use the correct top-level resource name 'marine-investigative' *** return _baseService.postMultipart( baseUrl: baseUrl, - endpoint: 'marine/investigative/images/send-email', + endpoint: 'marine-investigative/images/send-email', fields: fields, files: {}, ); diff --git a/lib/services/marine_investigative_sampling_service.dart b/lib/services/marine_investigative_sampling_service.dart index bbdde22..7f06b29 100644 --- a/lib/services/marine_investigative_sampling_service.dart +++ b/lib/services/marine_investigative_sampling_service.dart @@ -732,7 +732,10 @@ class MarineInvestigativeSamplingService { final allLimits = await _dbHelper.loadMarineParameterLimits() ?? []; if (allLimits.isEmpty) return ""; - final dynamic stationId = data.selectedStation?['man_station_id']; + // --- START FIX: Use correct key 'station_id' with fallback to 'man_station_id' --- + final dynamic stationId = data.selectedStation?['station_id'] ?? data.selectedStation?['man_station_id']; + // --- END FIX --- + if (stationId == null) return ""; // Cannot check limits final readings = { @@ -843,7 +846,7 @@ class MarineInvestigativeSamplingService { if (isHit) { final valueStr = value.toStringAsFixed(5); final lowerStr = lowerLimit?.toStringAsFixed(5) ?? 'N/A'; - final upperStr = upperLimit?.toStringAsFixed(5) ?? 'N/á'; + final upperStr = upperLimit?.toStringAsFixed(5) ?? 'N/A'; String limitStr; if (lowerStr != 'N/A' && upperStr != 'N/A') { limitStr = '$lowerStr - $upperStr'; @@ -864,7 +867,7 @@ class MarineInvestigativeSamplingService { final buffer = StringBuffer() ..writeln() ..writeln(' ') - ..writeln('🚨 *NPE Parameter Limit Detected:*') + ..writeln('🚨 *Marine NPE Parameter Limit Detected:*') ..writeln('The following parameters triggered an NPE alert:'); buffer.writeAll(npeMessages, '\n');