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));
} 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<MarineInvesManualStep2S
Text("Required Photos *", style: Theme.of(context).textTheme.titleLarge),
// MODIFIED: Matched in-situ 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)
),
const SizedBox(height: 8),

View File

@ -28,12 +28,12 @@ class NPEReportFromInSitu extends StatefulWidget {
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>();
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<NPEReportFromInSitu> {
String? _selectedState;
String? _selectedCategory;
Map<String, dynamic>? _selectedManualStation;
// --- END: MODIFIED STATE VARIABLES ---
// Controllers
final _stationIdController = TextEditingController();
@ -81,6 +80,8 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
@override
void initState() {
super.initState();
// Added: Register observer
WidgetsBinding.instance.addObserver(this);
_samplingService = Provider.of<MarineInSituSamplingService>(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<NPEReportFromInSitu> {
@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<NPEReportFromInSitu> {
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<NPEReportFromInSitu> {
_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<void> _fetchRecentNearbySamples() async {
setState(() => _isLoadingRecentSamples = true);
@ -269,10 +285,7 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
_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<NPEReportFromInSitu> {
if (mounted) setState(() => _isPickingImage = false);
}
// --- START: IN-SITU DEVICE METHODS (Unchanged) ---
// --- IN-SITU DEVICE METHODS ---
void _updateTextFields(Map<String, double> readings) {
const defaultValue = -999.0;
setState(() {
@ -433,7 +446,7 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
}
Future<bool> _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<NPEReportFromInSitu> {
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<NPEReportFromInSitu> {
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<NPEReportFromInSitu> {
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<NPEReportFromInSitu> {
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<NPEReportFromInSitu> {
onChanged: (value) {
setState(() {
_useRecentSample = value;
_clearRecentSampleSelection(); // Clear "Yes" path data
_clearRecentSampleSelection();
});
},
),
@ -601,10 +613,8 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
],
),
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<NPEReportFromInSitu> {
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<NPEReportFromInSitu> {
),
],
// "NO" PATH: Select from manual station list
if (_useRecentSample == false) ...[
_buildSectionTitle("2. Select Manual Station"),
DropdownSearch<String>(
@ -687,7 +696,7 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
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<NPEReportFromInSitu> {
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<NPEReportFromInSitu> {
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<NPEReportFromInSitu> {
);
}
// --- 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<NPEReportFromInSitu> {
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<NPEReportFromInSitu> {
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<NPEReportFromInSitu> {
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 ---
}

View File

@ -26,7 +26,8 @@ class NPEReportFromTarball extends StatefulWidget {
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>();
bool _isLoading = false;
bool _isPickingImage = false;
@ -70,6 +71,8 @@ class _NPEReportFromTarballState extends State<NPEReportFromTarball> {
@override
void initState() {
super.initState();
// Added: Register observer
WidgetsBinding.instance.addObserver(this);
_samplingService = Provider.of<MarineInSituSamplingService>(context, listen: false);
WidgetsBinding.instance.addPostFrameCallback((_) {
@ -83,6 +86,8 @@ class _NPEReportFromTarballState extends State<NPEReportFromTarball> {
@override
void dispose() {
// Added: Remove observer
WidgetsBinding.instance.removeObserver(this);
_stationIdController.dispose();
_locationController.dispose();
_eventDateTimeController.dispose();
@ -106,6 +111,23 @@ class _NPEReportFromTarballState extends State<NPEReportFromTarball> {
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<NPEReportFromTarball> {
setState(() {
_isAutoReading = 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();
}
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>();
bool _isLoading = false;
bool _isPickingImage = false;
@ -67,6 +68,8 @@ class _NPEReportNewLocationState extends State<NPEReportNewLocation> {
@override
void initState() {
super.initState();
// Added: Register observer
WidgetsBinding.instance.addObserver(this);
_samplingService = Provider.of<MarineInSituSamplingService>(context, listen: false);
_setDefaultDateTime();
_loadAllStatesFromProvider();
@ -74,6 +77,8 @@ class _NPEReportNewLocationState extends State<NPEReportNewLocation> {
@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<NPEReportNewLocation> {
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<NPEReportNewLocation> {
break;
}
});
} else {
await _showImageErrorDialog(
"Image processing failed. Please ensure the photo is taken in landscape mode."
);
}
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) {
const defaultValue = -999.0;
setState(() {
@ -406,6 +480,7 @@ class _NPEReportNewLocationState extends State<NPEReportNewLocation> {
setState(() {
_isAutoReading = 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 '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: {},
);

View File

@ -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');