fix issue on marine investigative and npe report

This commit is contained in:
ALim Aidrus 2025-11-21 21:37:32 +08:00
parent d0f9d72ebd
commit 6c4bc335b8
6 changed files with 151 additions and 53 deletions

View File

@ -67,7 +67,7 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
setState(() => setImageCallback(file)); setState(() => setImageCallback(file));
} else if (mounted) { } else if (mounted) {
// Corrected snackbar message // 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) { if (mounted) {
@ -150,7 +150,7 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
Text("Required Photos *", style: Theme.of(context).textTheme.titleLarge), Text("Required Photos *", style: Theme.of(context).textTheme.titleLarge),
// MODIFIED: Matched in-situ text // MODIFIED: Matched in-situ text
const Text( const Text(
"All photos must be in landscape (vertical) orientation. A watermark will be applied automatically.", "All photos must be in landscape (horizontal) orientation. A watermark will be applied automatically.",
style: TextStyle(color: Colors.grey) style: TextStyle(color: Colors.grey)
), ),
const SizedBox(height: 8), const SizedBox(height: 8),

View File

@ -28,12 +28,12 @@ class NPEReportFromInSitu extends StatefulWidget {
State<NPEReportFromInSitu> createState() => _NPEReportFromInSituState(); State<NPEReportFromInSitu> createState() => _NPEReportFromInSituState();
} }
class _NPEReportFromInSituState extends State<NPEReportFromInSitu> { // Modified: Added WidgetsBindingObserver to handle app lifecycle changes (USB permission dialog)
class _NPEReportFromInSituState extends State<NPEReportFromInSitu> with WidgetsBindingObserver {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
bool _isLoading = false; bool _isLoading = false;
bool _isPickingImage = false; bool _isPickingImage = false;
// --- START: MODIFIED STATE VARIABLES ---
// Data handling // Data handling
bool? _useRecentSample; // To track Yes/No selection bool? _useRecentSample; // To track Yes/No selection
bool _isLoadingRecentSamples = false; // Now triggered on-demand bool _isLoadingRecentSamples = false; // Now triggered on-demand
@ -48,7 +48,6 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
String? _selectedState; String? _selectedState;
String? _selectedCategory; String? _selectedCategory;
Map<String, dynamic>? _selectedManualStation; Map<String, dynamic>? _selectedManualStation;
// --- END: MODIFIED STATE VARIABLES ---
// Controllers // Controllers
final _stationIdController = TextEditingController(); final _stationIdController = TextEditingController();
@ -81,6 +80,8 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Added: Register observer
WidgetsBinding.instance.addObserver(this);
_samplingService = Provider.of<MarineInSituSamplingService>(context, listen: false); _samplingService = Provider.of<MarineInSituSamplingService>(context, listen: false);
_loadAllStatesFromProvider(); // Load manual stations for "No" path _loadAllStatesFromProvider(); // Load manual stations for "No" path
_setDefaultDateTime(); // Set default time for all paths _setDefaultDateTime(); // Set default time for all paths
@ -88,6 +89,8 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
@override @override
void dispose() { void dispose() {
// Added: Remove observer
WidgetsBinding.instance.removeObserver(this);
_dataSubscription?.cancel(); _dataSubscription?.cancel();
_lockoutTimer?.cancel(); _lockoutTimer?.cancel();
if (_samplingService.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) { if (_samplingService.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
@ -117,7 +120,23 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
super.dispose(); 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() { void _setDefaultDateTime() {
final now = DateTime.now(); final now = DateTime.now();
_eventDateTimeController.text = DateFormat('yyyy-MM-dd HH:mm').format(now); _eventDateTimeController.text = DateFormat('yyyy-MM-dd HH:mm').format(now);
@ -167,19 +186,16 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
_latController.clear(); _latController.clear();
_longController.clear(); _longController.clear();
// --- CHANGE: Clear measurement controllers so they are empty before reading ---
_doPercentController.clear(); _doPercentController.clear();
_doMgLController.clear(); _doMgLController.clear();
_phController.clear(); _phController.clear();
_condController.clear(); _condController.clear();
_turbController.clear(); _turbController.clear();
_tempController.clear(); _tempController.clear();
// ---------------------------------------------------------------------------
_setDefaultDateTime(); // Reset to 'now' _setDefaultDateTime(); // Reset to 'now'
}); });
} }
// --- END: ADDED HELPER METHODS ---
Future<void> _fetchRecentNearbySamples() async { Future<void> _fetchRecentNearbySamples() async {
setState(() => _isLoadingRecentSamples = true); setState(() => _isLoadingRecentSamples = true);
@ -269,10 +285,7 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
_npeData.latitude = _latController.text; _npeData.latitude = _latController.text;
_npeData.longitude = _longController.text; _npeData.longitude = _longController.text;
// selectedStation is already set by either _populateFormFromData or the "No" path dropdown _npeData.locationDescription = _locationController.text;
// _npeData.selectedStation = _selectedRecentSample?.selectedStation;
_npeData.locationDescription = _locationController.text; // Used by both paths
_npeData.possibleSource = _possibleSourceController.text; _npeData.possibleSource = _possibleSourceController.text;
_npeData.othersObservationRemark = _othersObservationController.text; _npeData.othersObservationRemark = _othersObservationController.text;
_npeData.oxygenSaturation = double.tryParse(_doPercentController.text); _npeData.oxygenSaturation = double.tryParse(_doPercentController.text);
@ -387,7 +400,7 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
if (mounted) setState(() => _isPickingImage = false); if (mounted) setState(() => _isPickingImage = false);
} }
// --- START: IN-SITU DEVICE METHODS (Unchanged) --- // --- IN-SITU DEVICE METHODS ---
void _updateTextFields(Map<String, double> readings) { void _updateTextFields(Map<String, double> readings) {
const defaultValue = -999.0; const defaultValue = -999.0;
setState(() { setState(() {
@ -433,7 +446,7 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
} }
Future<bool> _connectToDevice(String type) async { Future<bool> _connectToDevice(String type) async {
setState(() => _isLoading = true); // Use main loading indicator setState(() => _isLoading = true);
bool success = false; bool success = false;
try { try {
if (type == 'bluetooth') { if (type == 'bluetooth') {
@ -463,7 +476,7 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
debugPrint("Connection failed: $e"); debugPrint("Connection failed: $e");
if (mounted) _showConnectionFailedDialog(); if (mounted) _showConnectionFailedDialog();
} finally { } finally {
if (mounted) setState(() => _isLoading = false); // Stop main loading indicator if (mounted) setState(() => _isLoading = false);
} }
return success; return success;
} }
@ -547,11 +560,10 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
setState(() { setState(() {
_isAutoReading = false; _isAutoReading = false;
_isLockedOut = false; _isLockedOut = false;
_isLoading = false; // Modified: Reset isLoading to unlock buttons
}); });
} }
} }
// --- END: IN-SITU DEVICE METHODS (Unchanged) ---
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -565,7 +577,7 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
child: ListView( child: ListView(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
children: [ children: [
// --- START: SECTION 1 (NEW) --- // --- SECTION 1 ---
_buildSectionTitle("1. Use Recent Sample?"), _buildSectionTitle("1. Use Recent Sample?"),
Row( Row(
children: [ children: [
@ -577,9 +589,9 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_useRecentSample = value; _useRecentSample = value;
_clearManualStationSelection(); // Clear "No" path data _clearManualStationSelection();
if (value == true && _recentNearbySamples.isEmpty) { if (value == true && _recentNearbySamples.isEmpty) {
_fetchRecentNearbySamples(); // Fetch samples on-demand _fetchRecentNearbySamples();
} }
}); });
}, },
@ -593,7 +605,7 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_useRecentSample = value; _useRecentSample = value;
_clearRecentSampleSelection(); // Clear "Yes" path data _clearRecentSampleSelection();
}); });
}, },
), ),
@ -601,10 +613,8 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// --- END: SECTION 1 (NEW) ---
// --- START: SECTION 2 (CONDITIONAL) --- // --- SECTION 2 ---
// "YES" PATH: Select from recent samples
if (_useRecentSample == true) ...[ if (_useRecentSample == true) ...[
_buildSectionTitle("2. Select Recent Sample"), _buildSectionTitle("2. Select Recent Sample"),
if (_isLoadingRecentSamples) if (_isLoadingRecentSamples)
@ -622,7 +632,7 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
if (sample != null) { if (sample != null) {
setState(() { setState(() {
_selectedRecentSample = sample; _selectedRecentSample = sample;
_npeData.selectedStation = sample.selectedStation; // CRITICAL: Set station for submission _npeData.selectedStation = sample.selectedStation;
_populateFormFromData(sample); _populateFormFromData(sample);
}); });
} }
@ -631,7 +641,6 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
), ),
], ],
// "NO" PATH: Select from manual station list
if (_useRecentSample == false) ...[ if (_useRecentSample == false) ...[
_buildSectionTitle("2. Select Manual Station"), _buildSectionTitle("2. Select Manual Station"),
DropdownSearch<String>( DropdownSearch<String>(
@ -687,7 +696,7 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
onChanged: (station) { onChanged: (station) {
setState(() { setState(() {
_selectedManualStation = station; _selectedManualStation = station;
_npeData.selectedStation = station; // CRITICAL: Set station for submission _npeData.selectedStation = station;
_stationIdController.text = station?['man_station_code'] ?? ''; _stationIdController.text = station?['man_station_code'] ?? '';
_locationController.text = station?['man_station_name'] ?? ''; _locationController.text = station?['man_station_name'] ?? '';
_latController.text = station?['man_latitude']?.toString() ?? ''; _latController.text = station?['man_latitude']?.toString() ?? '';
@ -697,9 +706,8 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
validator: (val) => val == null && _stationsForCategory.isNotEmpty ? "Station is required" : null, 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), const SizedBox(height: 24),
_buildSectionTitle("Station Information"), _buildSectionTitle("Station Information"),
_buildTextFormField(controller: _stationIdController, label: "Station ID", readOnly: true), _buildTextFormField(controller: _stationIdController, label: "Station ID", readOnly: true),
@ -734,12 +742,10 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15) padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15)
), ),
// Disable button if "Yes/No" hasn't been selected
onPressed: _isLoading || _useRecentSample == null ? null : _submitNpeReport, onPressed: _isLoading || _useRecentSample == null ? null : _submitNpeReport,
child: _isLoading ? const CircularProgressIndicator(color: Colors.white) : const Text("Submit Report"), child: _isLoading ? const CircularProgressIndicator(color: Colors.white) : const Text("Submit Report"),
), ),
), ),
// --- END: SHARED SECTIONS ---
], ],
), ),
), ),
@ -890,13 +896,10 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
); );
} }
// --- START: WIDGET BUILDERS FOR IN-SITU (Unchanged) ---
Widget _buildInSituSection() { Widget _buildInSituSection() {
final activeConnection = _getActiveConnectionDetails(); final activeConnection = _getActiveConnectionDetails();
final String? activeType = activeConnection?['type'] as String?; 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); final bool areFieldsReadOnly = (_useRecentSample == true);
return Column( return Column(
@ -987,7 +990,7 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
required String label, required String label,
required String unit, required String unit,
required TextEditingController controller, required TextEditingController controller,
bool readOnly = false, // ADDED: readOnly parameter bool readOnly = false,
}) { }) {
final bool isMissing = controller.text.isEmpty || controller.text.contains('-999'); final bool isMissing = controller.text.isEmpty || controller.text.contains('-999');
final String displayValue = isMissing ? '-.--' : (double.tryParse(controller.text)?.toStringAsFixed(5) ?? '-.--'); final String displayValue = isMissing ? '-.--' : (double.tryParse(controller.text)?.toStringAsFixed(5) ?? '-.--');
@ -1001,14 +1004,12 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
trailing: SizedBox( trailing: SizedBox(
width: 120, width: 120,
child: TextFormField( child: TextFormField(
// --- START: MODIFIED to handle readOnly vs. editable ---
controller: readOnly ? null : controller, controller: readOnly ? null : controller,
initialValue: readOnly ? displayValue : null, initialValue: readOnly ? displayValue : null,
key: readOnly ? ValueKey(displayValue) : null, key: readOnly ? ValueKey(displayValue) : null,
// --- END: MODIFIED ---
readOnly: readOnly, readOnly: readOnly,
textAlign: TextAlign.right, 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( style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: isMissing ? Colors.grey : Theme.of(context).colorScheme.primary, color: isMissing ? Colors.grey : Theme.of(context).colorScheme.primary,
@ -1016,16 +1017,13 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
decoration: const InputDecoration( decoration: const InputDecoration(
border: InputBorder.none, border: InputBorder.none,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
isDense: true, // Helps with alignment isDense: true,
// --- CHANGE: Added hint text to display when controller is empty (before start reading) ---
hintText: '-.--', hintText: '-.--',
hintStyle: TextStyle(color: Colors.grey), hintStyle: TextStyle(color: Colors.grey),
// -----------------------------------------------------------------------------------------
), ),
), ),
), ),
), ),
); );
} }
// --- END: WIDGET BUILDERS FOR IN-SITU ---
} }

View File

@ -26,7 +26,8 @@ class NPEReportFromTarball extends StatefulWidget {
State<NPEReportFromTarball> createState() => _NPEReportFromTarballState(); State<NPEReportFromTarball> createState() => _NPEReportFromTarballState();
} }
class _NPEReportFromTarballState extends State<NPEReportFromTarball> { // Modified: Added WidgetsBindingObserver to handle app lifecycle changes (USB permission dialog)
class _NPEReportFromTarballState extends State<NPEReportFromTarball> with WidgetsBindingObserver {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
bool _isLoading = false; bool _isLoading = false;
bool _isPickingImage = false; bool _isPickingImage = false;
@ -70,6 +71,8 @@ class _NPEReportFromTarballState extends State<NPEReportFromTarball> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Added: Register observer
WidgetsBinding.instance.addObserver(this);
_samplingService = Provider.of<MarineInSituSamplingService>(context, listen: false); _samplingService = Provider.of<MarineInSituSamplingService>(context, listen: false);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@ -83,6 +86,8 @@ class _NPEReportFromTarballState extends State<NPEReportFromTarball> {
@override @override
void dispose() { void dispose() {
// Added: Remove observer
WidgetsBinding.instance.removeObserver(this);
_stationIdController.dispose(); _stationIdController.dispose();
_locationController.dispose(); _locationController.dispose();
_eventDateTimeController.dispose(); _eventDateTimeController.dispose();
@ -106,6 +111,23 @@ class _NPEReportFromTarballState extends State<NPEReportFromTarball> {
super.dispose(); 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() { void _setDefaultDateTime() {
final now = DateTime.now(); final now = DateTime.now();
_eventDateTimeController.text = DateFormat('yyyy-MM-dd HH:mm').format(now); _eventDateTimeController.text = DateFormat('yyyy-MM-dd HH:mm').format(now);
@ -409,6 +431,7 @@ class _NPEReportFromTarballState extends State<NPEReportFromTarball> {
setState(() { setState(() {
_isAutoReading = false; _isAutoReading = false;
_isLockedOut = false; _isLockedOut = false;
_isLoading = false; // Added: Reset isLoading to unlock buttons
}); });
} }
} }

View File

@ -27,7 +27,8 @@ class NPEReportNewLocation extends StatefulWidget {
State<NPEReportNewLocation> createState() => _NPEReportNewLocationState(); State<NPEReportNewLocation> createState() => _NPEReportNewLocationState();
} }
class _NPEReportNewLocationState extends State<NPEReportNewLocation> { // Modified: Added WidgetsBindingObserver to handle app lifecycle changes (USB permission dialog)
class _NPEReportNewLocationState extends State<NPEReportNewLocation> with WidgetsBindingObserver {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
bool _isLoading = false; bool _isLoading = false;
bool _isPickingImage = false; bool _isPickingImage = false;
@ -67,6 +68,8 @@ class _NPEReportNewLocationState extends State<NPEReportNewLocation> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Added: Register observer
WidgetsBinding.instance.addObserver(this);
_samplingService = Provider.of<MarineInSituSamplingService>(context, listen: false); _samplingService = Provider.of<MarineInSituSamplingService>(context, listen: false);
_setDefaultDateTime(); _setDefaultDateTime();
_loadAllStatesFromProvider(); _loadAllStatesFromProvider();
@ -74,6 +77,8 @@ class _NPEReportNewLocationState extends State<NPEReportNewLocation> {
@override @override
void dispose() { void dispose() {
// Added: Remove observer
WidgetsBinding.instance.removeObserver(this);
_dataSubscription?.cancel(); _dataSubscription?.cancel();
_lockoutTimer?.cancel(); _lockoutTimer?.cancel();
if (_samplingService.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) { if (_samplingService.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
@ -102,6 +107,23 @@ class _NPEReportNewLocationState extends State<NPEReportNewLocation> {
super.dispose(); 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() { void _setDefaultDateTime() {
final now = DateTime.now(); final now = DateTime.now();
_eventDateTimeController.text = DateFormat('yyyy-MM-dd HH:mm').format(now); _eventDateTimeController.text = DateFormat('yyyy-MM-dd HH:mm').format(now);
@ -243,10 +265,62 @@ class _NPEReportNewLocationState extends State<NPEReportNewLocation> {
break; break;
} }
}); });
} else {
await _showImageErrorDialog(
"Image processing failed. Please ensure the photo is taken in landscape mode."
);
} }
if (mounted) setState(() => _isPickingImage = false); if (mounted) setState(() => _isPickingImage = false);
} }
Future<void> _showImageErrorDialog(String message) async {
if (!mounted) return;
return showDialog<void>(
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: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(dialogContext).pop();
},
),
],
);
},
);
}
void _updateTextFields(Map<String, double> readings) { void _updateTextFields(Map<String, double> readings) {
const defaultValue = -999.0; const defaultValue = -999.0;
setState(() { setState(() {
@ -406,6 +480,7 @@ class _NPEReportNewLocationState extends State<NPEReportNewLocation> {
setState(() { setState(() {
_isAutoReading = false; _isAutoReading = false;
_isLockedOut = false; _isLockedOut = false;
_isLoading = false; // Added: Reset isLoading to unlock buttons
}); });
} }
} }

View File

@ -1,5 +1,3 @@
// lib/services/marine_api_service.dart
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:intl/intl.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) // 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); final String stationTypeParam = Uri.encodeComponent(stationType);
// *** FIX: Use the correct top-level resource name 'marine-investigative' ***
final String endpoint = 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"); debugPrint("MarineApiService: Calling API endpoint: $endpoint");
final response = await _baseService.get(baseUrl, endpoint); final response = await _baseService.get(baseUrl, endpoint);
@ -184,10 +183,10 @@ class MarineApiService {
'samplingDate': samplingDate, 'samplingDate': samplingDate,
}; };
// Use a new endpoint dedicated to the investigative module // *** FIX: Use the correct top-level resource name 'marine-investigative' ***
return _baseService.postMultipart( return _baseService.postMultipart(
baseUrl: baseUrl, baseUrl: baseUrl,
endpoint: 'marine/investigative/images/send-email', endpoint: 'marine-investigative/images/send-email',
fields: fields, fields: fields,
files: {}, files: {},
); );

View File

@ -732,7 +732,10 @@ class MarineInvestigativeSamplingService {
final allLimits = await _dbHelper.loadMarineParameterLimits() ?? []; final allLimits = await _dbHelper.loadMarineParameterLimits() ?? [];
if (allLimits.isEmpty) return ""; 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 if (stationId == null) return ""; // Cannot check limits
final readings = { final readings = {
@ -843,7 +846,7 @@ class MarineInvestigativeSamplingService {
if (isHit) { if (isHit) {
final valueStr = value.toStringAsFixed(5); final valueStr = value.toStringAsFixed(5);
final lowerStr = lowerLimit?.toStringAsFixed(5) ?? 'N/A'; final lowerStr = lowerLimit?.toStringAsFixed(5) ?? 'N/A';
final upperStr = upperLimit?.toStringAsFixed(5) ?? 'N/á'; final upperStr = upperLimit?.toStringAsFixed(5) ?? 'N/A';
String limitStr; String limitStr;
if (lowerStr != 'N/A' && upperStr != 'N/A') { if (lowerStr != 'N/A' && upperStr != 'N/A') {
limitStr = '$lowerStr - $upperStr'; limitStr = '$lowerStr - $upperStr';
@ -864,7 +867,7 @@ class MarineInvestigativeSamplingService {
final buffer = StringBuffer() final buffer = StringBuffer()
..writeln() ..writeln()
..writeln(' ') ..writeln(' ')
..writeln('🚨 *NPE Parameter Limit Detected:*') ..writeln('🚨 *Marine NPE Parameter Limit Detected:*')
..writeln('The following parameters triggered an NPE alert:'); ..writeln('The following parameters triggered an NPE alert:');
buffer.writeAll(npeMessages, '\n'); buffer.writeAll(npeMessages, '\n');