repair marine investigative module
This commit is contained in:
parent
fa5f0361de
commit
882386c522
@ -32,6 +32,7 @@ class InSituSamplingData {
|
|||||||
String? weather;
|
String? weather;
|
||||||
String? tideLevel;
|
String? tideLevel;
|
||||||
String? seaCondition;
|
String? seaCondition;
|
||||||
|
// String? tarball; // <-- REMOVED THIS PROPERTY
|
||||||
String? eventRemarks;
|
String? eventRemarks;
|
||||||
String? labRemarks;
|
String? labRemarks;
|
||||||
|
|
||||||
@ -201,6 +202,7 @@ class InSituSamplingData {
|
|||||||
data.weather = json['weather'];
|
data.weather = json['weather'];
|
||||||
data.tideLevel = json['tide_level'];
|
data.tideLevel = json['tide_level'];
|
||||||
data.seaCondition = json['sea_condition'];
|
data.seaCondition = json['sea_condition'];
|
||||||
|
// data.tarball = json['tarball']; // <-- REMOVED DESERIALIZATION
|
||||||
data.eventRemarks = json['event_remarks'];
|
data.eventRemarks = json['event_remarks'];
|
||||||
data.labRemarks = json['lab_remarks'];
|
data.labRemarks = json['lab_remarks'];
|
||||||
data.optionalRemark1 = json['man_optional_photo_01_remarks'];
|
data.optionalRemark1 = json['man_optional_photo_01_remarks'];
|
||||||
@ -254,8 +256,8 @@ class InSituSamplingData {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a single JSON object with all submission data for offline storage.
|
/// Creates a Map object with all submission data for local logging.
|
||||||
Map<String, dynamic> toDbJson() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
'first_sampler_name': firstSamplerName,
|
'first_sampler_name': firstSamplerName,
|
||||||
'first_sampler_user_id': firstSamplerUserId,
|
'first_sampler_user_id': firstSamplerUserId,
|
||||||
@ -276,6 +278,7 @@ class InSituSamplingData {
|
|||||||
'weather': weather,
|
'weather': weather,
|
||||||
'tide_level': tideLevel,
|
'tide_level': tideLevel,
|
||||||
'sea_condition': seaCondition,
|
'sea_condition': seaCondition,
|
||||||
|
// 'tarball': tarball, // <-- REMOVED
|
||||||
'event_remarks': eventRemarks,
|
'event_remarks': eventRemarks,
|
||||||
'lab_remarks': labRemarks,
|
'lab_remarks': labRemarks,
|
||||||
'man_optional_photo_01_remarks': optionalRemark1,
|
'man_optional_photo_01_remarks': optionalRemark1,
|
||||||
@ -305,8 +308,101 @@ class InSituSamplingData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- REMOVED: generateTelegramAlertMessage method ---
|
/// Creates a single JSON object with all submission data, mimicking 'db.json'
|
||||||
// This logic is now in MarineInSituSamplingService
|
String toDbJson() {
|
||||||
|
// This is a direct conversion of the model's properties to a map,
|
||||||
|
// with keys matching the expected 'db.json' file format.
|
||||||
|
final data = {
|
||||||
|
'battery_cap': batteryVoltage == -999.0 ? null : batteryVoltage,
|
||||||
|
'device_name': sondeId,
|
||||||
|
'sampling_type': samplingType,
|
||||||
|
'report_id': reportId,
|
||||||
|
'sampler_2ndname': secondSampler?['first_name'],
|
||||||
|
'sample_state': selectedStateName,
|
||||||
|
'station_id': selectedStation?['man_station_code'],
|
||||||
|
'tech_id': firstSamplerUserId,
|
||||||
|
'tech_phonenum': null, // This field was not in the model
|
||||||
|
'tech_name': firstSamplerName,
|
||||||
|
'latitude': stationLatitude,
|
||||||
|
'longitude': stationLongitude,
|
||||||
|
'record_dt': '$samplingDate $samplingTime',
|
||||||
|
'do_mgl': oxygenConcentration == -999.0 ? null : oxygenConcentration,
|
||||||
|
'do_sat': oxygenSaturation == -999.0 ? null : oxygenSaturation,
|
||||||
|
'ph': ph == -999.0 ? null : ph,
|
||||||
|
'salinity': salinity == -999.0 ? null : salinity,
|
||||||
|
'tss': tss == -999.0 ? null : tss,
|
||||||
|
'temperature': temperature == -999.0 ? null : temperature,
|
||||||
|
'turbidity': turbidity == -999.0 ? null : turbidity,
|
||||||
|
'tds': tds == -999.0 ? null : tds,
|
||||||
|
'electric_conductivity': electricalConductivity == -999.0 ? null : electricalConductivity,
|
||||||
|
'sample_id': sampleIdCode,
|
||||||
|
'tarball': "", // <-- MODIFIED: Hardcoded to "N/A"
|
||||||
|
'weather': weather,
|
||||||
|
'tide_lvl': tideLevel,
|
||||||
|
'sea_cond': seaCondition,
|
||||||
|
'remarks_event': eventRemarks,
|
||||||
|
'remarks_lab': labRemarks,
|
||||||
|
};
|
||||||
|
// Remove null values before encoding
|
||||||
|
data.removeWhere((key, value) => value == null);
|
||||||
|
return jsonEncode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a JSON object for basic form info, mimicking 'marine_insitu_basic_form.json'.
|
||||||
|
String toBasicFormJson() {
|
||||||
|
final data = {
|
||||||
|
'tech_name': firstSamplerName,
|
||||||
|
'sampler_2ndname': secondSampler?['first_name'], // Assuming 'first_name' key
|
||||||
|
'sample_date': samplingDate,
|
||||||
|
'sample_time': samplingTime,
|
||||||
|
'sampling_type': samplingType,
|
||||||
|
'sample_state': selectedStateName,
|
||||||
|
'station_id': selectedStation?['man_station_code'], // Marine-specific key
|
||||||
|
'station_latitude': stationLatitude,
|
||||||
|
'station_longitude': stationLongitude,
|
||||||
|
'latitude': currentLatitude,
|
||||||
|
'longitude': currentLongitude,
|
||||||
|
'sample_id': sampleIdCode,
|
||||||
|
};
|
||||||
|
// Remove null values before encoding
|
||||||
|
data.removeWhere((key, value) => value == null);
|
||||||
|
return jsonEncode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a JSON object for sensor readings, mimicking 'marine_sampling_reading.json'.
|
||||||
|
String toReadingJson() {
|
||||||
|
final data = {
|
||||||
|
'do_mgl': oxygenConcentration == -999.0 ? null : oxygenConcentration,
|
||||||
|
'do_sat': oxygenSaturation == -999.0 ? null : oxygenSaturation,
|
||||||
|
'ph': ph == -999.0 ? null : ph,
|
||||||
|
'salinity': salinity == -999.0 ? null : salinity,
|
||||||
|
'tds': tds == -999.0 ? null : tds,
|
||||||
|
'tss': tss == -999.0 ? null : tss,
|
||||||
|
'temperature': temperature == -999.0 ? null : temperature,
|
||||||
|
'turbidity': turbidity == -999.0 ? null : turbidity,
|
||||||
|
'electric_conductivity': electricalConductivity == -999.0 ? null : electricalConductivity,
|
||||||
|
'date_sampling_reading': dataCaptureDate,
|
||||||
|
'time_sampling_reading': dataCaptureTime,
|
||||||
|
};
|
||||||
|
// Remove null values before encoding
|
||||||
|
data.removeWhere((key, value) => value == null);
|
||||||
|
return jsonEncode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a JSON object for manual info, mimicking 'marine_manual_info.json'.
|
||||||
|
String toManualInfoJson() {
|
||||||
|
final data = {
|
||||||
|
'tarball': "", // <-- MODIFIED: Hardcoded to "N/A"
|
||||||
|
'weather': weather,
|
||||||
|
'tide_lvl': tideLevel,
|
||||||
|
'sea_cond': seaCondition,
|
||||||
|
'remarks_event': eventRemarks,
|
||||||
|
'remarks_lab': labRemarks,
|
||||||
|
};
|
||||||
|
// Remove null values before encoding
|
||||||
|
data.removeWhere((key, value) => value == null);
|
||||||
|
return jsonEncode(data);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, String> toApiFormData() {
|
Map<String, String> toApiFormData() {
|
||||||
final Map<String, String> map = {};
|
final Map<String, String> map = {};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart
|
// lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data'; // <-- ADDED: Required for Uint8List
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -26,7 +27,7 @@ class MarineInvesManualStep2SiteInfo extends StatefulWidget {
|
|||||||
|
|
||||||
class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2SiteInfo> {
|
class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2SiteInfo> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
bool _isPickingImage = false;
|
bool _isPickingImage = false; // <-- ADDED: State variable from in-situ
|
||||||
|
|
||||||
late final TextEditingController _eventRemarksController;
|
late final TextEditingController _eventRemarksController;
|
||||||
late final TextEditingController _labRemarksController;
|
late final TextEditingController _labRemarksController;
|
||||||
@ -49,6 +50,7 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: MODIFIED _setImage function (from in-situ) ---
|
||||||
/// Handles picking and processing an image using the dedicated service.
|
/// Handles picking and processing an image using the dedicated service.
|
||||||
void _setImage(Function(File?) setImageCallback, ImageSource source, String imageInfo, {required bool isRequired}) async {
|
void _setImage(Function(File?) setImageCallback, ImageSource source, String imageInfo, {required bool isRequired}) async {
|
||||||
if (_isPickingImage) return;
|
if (_isPickingImage) return;
|
||||||
@ -56,19 +58,23 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
|
|
||||||
final service = Provider.of<MarineInvestigativeSamplingService>(context, listen: false);
|
final service = Provider.of<MarineInvestigativeSamplingService>(context, listen: false);
|
||||||
|
|
||||||
// The service's pickAndProcessImage method will handle file naming
|
// Always pass `isRequired: true` to the service to enforce landscape check
|
||||||
final file = await service.pickAndProcessImage(source, data: widget.data, imageInfo: imageInfo, isRequired: isRequired);
|
// and watermarking for ALL photos (required or optional).
|
||||||
|
// The 'isRequired' param is just for the UI text.
|
||||||
|
final file = await service.pickAndProcessImage(source, data: widget.data, imageInfo: imageInfo, isRequired: true);
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
setState(() => setImageCallback(file));
|
setState(() => setImageCallback(file));
|
||||||
} else if (mounted) {
|
} else if (mounted) {
|
||||||
_showSnackBar('Image selection failed. Please ensure all photos are taken in landscape mode.', isError: true);
|
// Corrected snackbar message
|
||||||
|
_showSnackBar('Image selection failed. Please ensure all photos are taken in landscape (horizontal) mode.', isError: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _isPickingImage = false);
|
setState(() => _isPickingImage = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED _setImage function ---
|
||||||
|
|
||||||
/// Validates the form and all required images before proceeding.
|
/// Validates the form and all required images before proceeding.
|
||||||
void _goToNextStep() {
|
void _goToNextStep() {
|
||||||
@ -142,7 +148,11 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
|
|
||||||
// --- Section: Required Photos ---
|
// --- Section: Required Photos ---
|
||||||
Text("Required Photos *", style: Theme.of(context).textTheme.titleLarge),
|
Text("Required Photos *", style: Theme.of(context).textTheme.titleLarge),
|
||||||
const Text("All photos must be taken in landscape (horizontal) orientation.", style: TextStyle(color: Colors.grey)),
|
// MODIFIED: Matched in-situ text
|
||||||
|
const Text(
|
||||||
|
"All photos must be in landscape (horizontal) orientation. A watermark will be applied automatically.",
|
||||||
|
style: TextStyle(color: Colors.grey)
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildImagePicker('Left Side Land View', 'LEFT_LAND_VIEW', widget.data.leftLandViewImage, (file) => widget.data.leftLandViewImage = file, isRequired: true),
|
_buildImagePicker('Left Side Land View', 'LEFT_LAND_VIEW', widget.data.leftLandViewImage, (file) => widget.data.leftLandViewImage = file, isRequired: true),
|
||||||
_buildImagePicker('Right Side Land View', 'RIGHT_LAND_VIEW', widget.data.rightLandViewImage, (file) => widget.data.rightLandViewImage = file, isRequired: true),
|
_buildImagePicker('Right Side Land View', 'RIGHT_LAND_VIEW', widget.data.rightLandViewImage, (file) => widget.data.rightLandViewImage = file, isRequired: true),
|
||||||
@ -196,7 +206,8 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A reusable widget for picking and displaying an image
|
// --- START: MODIFIED _buildImagePicker (from in-situ) ---
|
||||||
|
/// A reusable widget for picking and displaying an image.
|
||||||
Widget _buildImagePicker(String title, String imageInfo, File? imageFile, Function(File?) setImageCallback, {bool isRequired = false}) {
|
Widget _buildImagePicker(String title, String imageInfo, File? imageFile, Function(File?) setImageCallback, {bool isRequired = false}) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
@ -207,14 +218,63 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (imageFile != null)
|
if (imageFile != null)
|
||||||
Stack(
|
Stack(
|
||||||
// ... (Image preview stack - same as original)
|
alignment: Alignment.topRight,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
child: FutureBuilder<Uint8List>(
|
||||||
|
// Use ValueKey to ensure FutureBuilder refetches when the file path changes
|
||||||
|
key: ValueKey(imageFile.path),
|
||||||
|
future: imageFile.readAsBytes(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return Container(
|
||||||
|
height: 150,
|
||||||
|
width: double.infinity,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) {
|
||||||
|
return Container(
|
||||||
|
height: 150,
|
||||||
|
width: double.infinity,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Icon(Icons.error, color: Colors.red, size: 40),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Display the image from memory
|
||||||
|
return Image.memory(
|
||||||
|
snapshot.data!,
|
||||||
|
height: 150,
|
||||||
|
width: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(color: Colors.black.withOpacity(0.6), shape: BoxShape.circle),
|
||||||
|
child: IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(Icons.close, color: Colors.white, size: 20),
|
||||||
|
onPressed: () => setState(() => setImageCallback(null)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Row(
|
Row(
|
||||||
// ... (Camera/Gallery buttons - same as original)
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
ElevatedButton.icon(onPressed: _isPickingImage ? null : () => _setImage(setImageCallback, ImageSource.camera, imageInfo, isRequired: isRequired), icon: const Icon(Icons.camera_alt), label: const Text("Camera")),
|
||||||
|
ElevatedButton.icon(onPressed: _isPickingImage ? null : () => _setImage(setImageCallback, ImageSource.gallery, imageInfo, isRequired: isRequired), icon: const Icon(Icons.photo_library), label: const Text("Gallery")),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED _buildImagePicker ---
|
||||||
}
|
}
|
||||||
@ -95,29 +95,22 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: MODIFIED LIFECYCLE METHOD ---
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
// Use the member variable, not context
|
|
||||||
final service = _samplingService;
|
final service = _samplingService;
|
||||||
final btConnecting = service.bluetoothConnectionState.value == BluetoothConnectionState.connecting;
|
final btConnecting = service.bluetoothConnectionState.value == BluetoothConnectionState.connecting;
|
||||||
final serialConnecting = service.serialConnectionState.value == SerialConnectionState.connecting;
|
final serialConnecting = service.serialConnectionState.value == SerialConnectionState.connecting;
|
||||||
|
|
||||||
// If the widget's local state is loading OR the service's state is stuck connecting
|
|
||||||
if (_isLoading || btConnecting || serialConnecting) {
|
if (_isLoading || btConnecting || serialConnecting) {
|
||||||
// Force-call disconnect to reset both the service's state
|
|
||||||
// and the local _isLoading flag (inside _disconnect).
|
|
||||||
_disconnectFromAll();
|
_disconnectFromAll();
|
||||||
} else {
|
} else {
|
||||||
// If not stuck, just a normal refresh
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- END: MODIFIED LIFECYCLE METHOD ---
|
|
||||||
|
|
||||||
void _initializeControllers() {
|
void _initializeControllers() {
|
||||||
widget.data.dataCaptureDate = widget.data.samplingDate;
|
widget.data.dataCaptureDate = widget.data.samplingDate;
|
||||||
@ -299,9 +292,7 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: MODIFIED DISCONNECT METHOD ---
|
|
||||||
void _disconnect(String type) {
|
void _disconnect(String type) {
|
||||||
// Use the member variable, not context, for lifecycle safety
|
|
||||||
final service = _samplingService;
|
final service = _samplingService;
|
||||||
if (type == 'bluetooth') {
|
if (type == 'bluetooth') {
|
||||||
service.disconnectFromBluetooth();
|
service.disconnectFromBluetooth();
|
||||||
@ -315,15 +306,12 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isAutoReading = false;
|
_isAutoReading = false;
|
||||||
_isLockedOut = false;
|
_isLockedOut = false;
|
||||||
_isLoading = false; // <-- CRITICAL: Also reset the loading flag
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- END: MODIFIED DISCONNECT METHOD ---
|
|
||||||
|
|
||||||
// --- START: MODIFIED DISCONNECT_ALL METHOD ---
|
|
||||||
void _disconnectFromAll() {
|
void _disconnectFromAll() {
|
||||||
// Use the member variable, not context, for lifecycle safety
|
|
||||||
final service = _samplingService;
|
final service = _samplingService;
|
||||||
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||||
_disconnect('bluetooth');
|
_disconnect('bluetooth');
|
||||||
@ -332,7 +320,6 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
_disconnect('serial');
|
_disconnect('serial');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- END: MODIFIED DISCONNECT_ALL METHOD ---
|
|
||||||
|
|
||||||
void _updateTextFields(Map<String, double> readings) {
|
void _updateTextFields(Map<String, double> readings) {
|
||||||
const defaultValue = -999.0;
|
const defaultValue = -999.0;
|
||||||
@ -350,6 +337,7 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: MODIFIED _validateAndProceed ---
|
||||||
void _validateAndProceed() {
|
void _validateAndProceed() {
|
||||||
if (_isLockedOut) {
|
if (_isLockedOut) {
|
||||||
_showSnackBar("Please wait for the initial reading period to complete.", isError: true);
|
_showSnackBar("Please wait for the initial reading period to complete.", isError: true);
|
||||||
@ -366,13 +354,25 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
_formKey.currentState!.save();
|
_formKey.currentState!.save();
|
||||||
|
|
||||||
final currentReadings = _captureReadingsToMap();
|
final currentReadings = _captureReadingsToMap();
|
||||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
List<Map<String, dynamic>> outOfBoundsParams = [];
|
||||||
final marineLimits = authProvider.marineParameterLimits ?? [];
|
|
||||||
final outOfBoundsParams = _validateParameters(currentReadings, marineLimits);
|
|
||||||
|
|
||||||
setState(() {
|
// --- NEW CONDITIONAL LOGIC ---
|
||||||
_outOfBoundsKeys = outOfBoundsParams.map((p) => _parameters.firstWhere((param) => param['label'] == p['label'])['key'] as String).toSet();
|
// Only check limits if it's a Manual Station
|
||||||
});
|
if (widget.data.stationTypeSelection == 'Existing Manual Station') {
|
||||||
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
final marineLimits = authProvider.marineParameterLimits ?? [];
|
||||||
|
outOfBoundsParams = _validateParameters(currentReadings, marineLimits);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_outOfBoundsKeys = outOfBoundsParams.map((p) => _parameters.firstWhere((param) => param['label'] == p['label'])['key'] as String).toSet();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If not a manual station, ensure any old highlights are cleared
|
||||||
|
setState(() {
|
||||||
|
_outOfBoundsKeys.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// --- END NEW CONDITIONAL LOGIC ---
|
||||||
|
|
||||||
if (outOfBoundsParams.isNotEmpty) {
|
if (outOfBoundsParams.isNotEmpty) {
|
||||||
_showParameterLimitDialog(outOfBoundsParams, currentReadings);
|
_showParameterLimitDialog(outOfBoundsParams, currentReadings);
|
||||||
@ -380,6 +380,7 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
_saveDataAndMoveOn(currentReadings);
|
_saveDataAndMoveOn(currentReadings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED _validateAndProceed ---
|
||||||
|
|
||||||
Map<String, double> _captureReadingsToMap() {
|
Map<String, double> _captureReadingsToMap() {
|
||||||
final Map<String, double> readings = {};
|
final Map<String, double> readings = {};
|
||||||
@ -394,16 +395,14 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
List<Map<String, dynamic>> _validateParameters(Map<String, double> readings, List<Map<String, dynamic>> limits) {
|
List<Map<String, dynamic>> _validateParameters(Map<String, double> readings, List<Map<String, dynamic>> limits) {
|
||||||
final List<Map<String, dynamic>> invalidParams = [];
|
final List<Map<String, dynamic>> invalidParams = [];
|
||||||
|
|
||||||
// --- MODIFIED: Get station ID based on station type ---
|
|
||||||
int? stationId;
|
int? stationId;
|
||||||
|
// This check is now redundant due to _validateAndProceed, but safe to keep
|
||||||
if (widget.data.stationTypeSelection == 'Existing Manual Station') {
|
if (widget.data.stationTypeSelection == 'Existing Manual Station') {
|
||||||
stationId = widget.data.selectedStation?['station_id'];
|
stationId = widget.data.selectedStation?['man_station_id'];
|
||||||
}
|
}
|
||||||
// Note: Add logic here if Tarball or New Locations have different limits
|
|
||||||
// For now, we only validate against manual station limits
|
|
||||||
|
|
||||||
debugPrint("--- Parameter Validation Start (Investigative) ---");
|
debugPrint("--- Parameter Validation Start (Investigative) ---");
|
||||||
debugPrint("Selected Station ID: $stationId");
|
debugPrint("Selected Station ID: $stationId (from 'man_station_id')");
|
||||||
|
|
||||||
double? _parseLimitValue(dynamic value) {
|
double? _parseLimitValue(dynamic value) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
@ -824,8 +823,11 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
if (isConnecting || _isLoading)
|
if (isConnecting || _isLoading)
|
||||||
const CircularProgressIndicator()
|
const CircularProgressIndicator()
|
||||||
else if (isConnected)
|
else if (isConnected)
|
||||||
Row(
|
Wrap(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
alignment: WrapAlignment.spaceEvenly,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
spacing: 8.0,
|
||||||
|
runSpacing: 4.0,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: Icon(_isAutoReading ? Icons.stop_circle_outlined : Icons.play_circle_outlined),
|
icon: Icon(_isAutoReading ? Icons.stop_circle_outlined : Icons.play_circle_outlined),
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
// lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_4_summary.dart
|
// lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_4_summary.dart
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data'; // <-- Required for Uint8List
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../../../auth_provider.dart';
|
import '../../../../auth_provider.dart';
|
||||||
import '../../../../models/marine_inves_manual_sampling_data.dart';
|
import '../../../../models/marine_inves_manual_sampling_data.dart';
|
||||||
// REMOVED: Import for NPE Report Screen is no longer needed
|
// REMOVED: Import for NPE Report Screen is no longer needed
|
||||||
// import '../reports/npe_report_from_investigative.dart';
|
|
||||||
|
|
||||||
class MarineInvesManualStep4Summary extends StatefulWidget {
|
class MarineInvesManualStep4Summary extends StatefulWidget {
|
||||||
final MarineInvesManualSamplingData data;
|
final MarineInvesManualSamplingData data;
|
||||||
@ -29,7 +29,6 @@ class MarineInvesManualStep4Summary extends StatefulWidget {
|
|||||||
class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Summary> {
|
class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Summary> {
|
||||||
bool _isHandlingSubmit = false;
|
bool _isHandlingSubmit = false;
|
||||||
|
|
||||||
// Keep parameter names for highlighting out-of-bounds station limits
|
|
||||||
static const Map<String, String> _parameterKeyToLimitName = {
|
static const Map<String, String> _parameterKeyToLimitName = {
|
||||||
'oxygenConcentration': 'Oxygen Conc',
|
'oxygenConcentration': 'Oxygen Conc',
|
||||||
'oxygenSaturation': 'Oxygen Sat',
|
'oxygenSaturation': 'Oxygen Sat',
|
||||||
@ -43,18 +42,15 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
|||||||
'batteryVoltage': 'Battery',
|
'batteryVoltage': 'Battery',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep this function to highlight parameters outside *station* limits
|
|
||||||
Set<String> _getOutOfBoundsKeys(BuildContext context) {
|
Set<String> _getOutOfBoundsKeys(BuildContext context) {
|
||||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||||
// Use regular marine limits, not NPE limits
|
|
||||||
final marineLimits = authProvider.marineParameterLimits ?? [];
|
final marineLimits = authProvider.marineParameterLimits ?? [];
|
||||||
final Set<String> invalidKeys = {};
|
final Set<String> invalidKeys = {};
|
||||||
|
|
||||||
int? stationId;
|
int? stationId;
|
||||||
if (widget.data.stationTypeSelection == 'Existing Manual Station') {
|
if (widget.data.stationTypeSelection == 'Existing Manual Station') {
|
||||||
stationId = widget.data.selectedStation?['station_id'];
|
stationId = widget.data.selectedStation?['man_station_id'];
|
||||||
}
|
}
|
||||||
// Note: Only checking against manual station limits for now.
|
|
||||||
|
|
||||||
final readings = {
|
final readings = {
|
||||||
'oxygenConcentration': widget.data.oxygenConcentration,
|
'oxygenConcentration': widget.data.oxygenConcentration,
|
||||||
@ -88,7 +84,7 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
|||||||
limitData = marineLimits.firstWhere(
|
limitData = marineLimits.firstWhere(
|
||||||
(l) =>
|
(l) =>
|
||||||
l['param_parameter_list'] == limitName &&
|
l['param_parameter_list'] == limitName &&
|
||||||
l['station_id'] == stationId,
|
l['station_id']?.toString() == stationId.toString(),
|
||||||
orElse: () => {},
|
orElse: () => {},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -107,17 +103,226 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
|||||||
return invalidKeys;
|
return invalidKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
// REMOVED: _getNpeTriggeredParameters method
|
// --- START: ADDED from in-situ module ---
|
||||||
// REMOVED: _showNpeDialog method
|
/// Checks captured data against NPE limits and returns detailed information.
|
||||||
|
List<Map<String, dynamic>> _getNpeTriggeredParameters(BuildContext context) {
|
||||||
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
final npeLimits = authProvider.npeParameterLimits ?? [];
|
||||||
|
if (npeLimits.isEmpty) return [];
|
||||||
|
|
||||||
/// Handles the complete submission flow WITHOUT NPE check.
|
final List<Map<String, dynamic>> triggeredParams = [];
|
||||||
|
|
||||||
|
final readings = {
|
||||||
|
'oxygenConcentration': widget.data.oxygenConcentration,
|
||||||
|
'oxygenSaturation': widget.data.oxygenSaturation,
|
||||||
|
'ph': widget.data.ph,
|
||||||
|
'salinity': widget.data.salinity,
|
||||||
|
'electricalConductivity': widget.data.electricalConductivity,
|
||||||
|
'temperature': widget.data.temperature,
|
||||||
|
'tds': widget.data.tds,
|
||||||
|
'turbidity': widget.data.turbidity,
|
||||||
|
'tss': widget.data.tss,
|
||||||
|
};
|
||||||
|
|
||||||
|
double? parseLimitValue(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is num) return value.toDouble();
|
||||||
|
if (value is String) return double.tryParse(value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
readings.forEach((key, value) {
|
||||||
|
if (value == null || value == -999.0) return;
|
||||||
|
final limitName = _parameterKeyToLimitName[key];
|
||||||
|
if (limitName == null) return;
|
||||||
|
|
||||||
|
// NPE limits are general and NOT station-specific
|
||||||
|
final limitData = npeLimits.firstWhere(
|
||||||
|
(l) => l['param_parameter_list'] == limitName,
|
||||||
|
orElse: () => {},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (limitData.isNotEmpty) {
|
||||||
|
final lowerLimit = parseLimitValue(limitData['param_lower_limit']);
|
||||||
|
final upperLimit = parseLimitValue(limitData['param_upper_limit']);
|
||||||
|
bool isHit = false;
|
||||||
|
|
||||||
|
if (lowerLimit != null && upperLimit != null) {
|
||||||
|
if (value >= lowerLimit && value <= upperLimit) isHit = true;
|
||||||
|
} else if (lowerLimit != null && upperLimit == null) {
|
||||||
|
if (value >= lowerLimit) isHit = true;
|
||||||
|
} else if (upperLimit != null && lowerLimit == null) {
|
||||||
|
if (value <= upperLimit) isHit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHit) {
|
||||||
|
triggeredParams.add({
|
||||||
|
'label': limitName,
|
||||||
|
'value': value,
|
||||||
|
'lower_limit': lowerLimit,
|
||||||
|
'upper_limit': upperLimit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return triggeredParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays a SIMPLIFIED NPE warning dialog (no "Open Report" button).
|
||||||
|
Future<void> _showNpeWarningDialog(
|
||||||
|
BuildContext context, List<Map<String, dynamic>> triggeredParams) async {
|
||||||
|
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (BuildContext dialogContext) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text("NPE Parameter Limit Detected"),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'The following parameters have fallen under the Non-Permissible Event (NPE) limit:'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Table(
|
||||||
|
columnWidths: const {
|
||||||
|
0: FlexColumnWidth(2),
|
||||||
|
1: FlexColumnWidth(2.5),
|
||||||
|
2: FlexColumnWidth(1.5),
|
||||||
|
},
|
||||||
|
border: TableBorder(
|
||||||
|
horizontalInside: BorderSide(
|
||||||
|
width: 0.5,
|
||||||
|
color: isDarkTheme
|
||||||
|
? Colors.grey.shade700
|
||||||
|
: Colors.grey.shade300),
|
||||||
|
verticalInside: BorderSide(
|
||||||
|
width: 0.5,
|
||||||
|
color: isDarkTheme
|
||||||
|
? Colors.grey.shade700
|
||||||
|
: Colors.grey.shade300),
|
||||||
|
top: BorderSide(
|
||||||
|
width: 1,
|
||||||
|
color: isDarkTheme
|
||||||
|
? Colors.grey.shade600
|
||||||
|
: Colors.grey.shade400),
|
||||||
|
bottom: BorderSide(
|
||||||
|
width: 1,
|
||||||
|
color: isDarkTheme
|
||||||
|
? Colors.grey.shade600
|
||||||
|
: Colors.grey.shade400),
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TableRow(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDarkTheme
|
||||||
|
? Colors.grey.shade800
|
||||||
|
: Colors.grey.shade200),
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: Text('Parameter',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleSmall
|
||||||
|
?.color))),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: Text('NPE Range',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleSmall
|
||||||
|
?.color))),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: Text('Current',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleSmall
|
||||||
|
?.color))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
...triggeredParams.map((p) {
|
||||||
|
final lowerStr =
|
||||||
|
p['lower_limit']?.toStringAsFixed(5) ?? 'N/A';
|
||||||
|
final upperStr =
|
||||||
|
p['upper_limit']?.toStringAsFixed(5) ?? 'N/A';
|
||||||
|
String range;
|
||||||
|
if (lowerStr != 'N/A' && upperStr != 'N/A') {
|
||||||
|
range = '$lowerStr - $upperStr';
|
||||||
|
} else if (lowerStr != 'N/A') {
|
||||||
|
range = '>= $lowerStr';
|
||||||
|
} else {
|
||||||
|
range = '<= $upperStr';
|
||||||
|
}
|
||||||
|
|
||||||
|
return TableRow(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: Text(p['label'])),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: Text(range)),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: Text(
|
||||||
|
p['value'].toStringAsFixed(5),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.redAccent,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text('Submission will proceed.'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
child: const Text("OK"),
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// --- END: ADDED from in-situ module ---
|
||||||
|
|
||||||
|
// --- START: MODIFIED _handleSubmit ---
|
||||||
|
/// Handles the complete submission flow WITH NPE check.
|
||||||
Future<void> _handleSubmit(BuildContext context) async {
|
Future<void> _handleSubmit(BuildContext context) async {
|
||||||
if (_isHandlingSubmit || widget.isLoading) return;
|
if (_isHandlingSubmit || widget.isLoading) return;
|
||||||
|
|
||||||
setState(() => _isHandlingSubmit = true);
|
setState(() => _isHandlingSubmit = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Directly call the submission function provided by the parent
|
// --- NPE CHECK ADDED ---
|
||||||
|
final npeParameters = _getNpeTriggeredParameters(context);
|
||||||
|
|
||||||
|
if (npeParameters.isNotEmpty) {
|
||||||
|
// Show the warning dialog and wait for the user to press "OK"
|
||||||
|
await _showNpeWarningDialog(context, npeParameters);
|
||||||
|
if (!mounted) return; // Check mount status after await
|
||||||
|
}
|
||||||
|
// --- END NPE CHECK ---
|
||||||
|
|
||||||
|
// Proceed with submission after dialog is dismissed (or if not needed)
|
||||||
final result = await widget.onSubmit();
|
final result = await widget.onSubmit();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
@ -133,6 +338,7 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
|||||||
|
|
||||||
// If submission was successful, navigate back to the home screen
|
// If submission was successful, navigate back to the home screen
|
||||||
if (result['success'] == true) {
|
if (result['success'] == true) {
|
||||||
|
// No conditional navigation, just go home
|
||||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
}
|
}
|
||||||
// If submission failed, the user stays on the summary screen to potentially retry
|
// If submission failed, the user stays on the summary screen to potentially retry
|
||||||
@ -154,6 +360,7 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED _handleSubmit ---
|
||||||
|
|
||||||
// Helper to build station details dynamically
|
// Helper to build station details dynamically
|
||||||
List<Widget> _buildStationDetails() {
|
List<Widget> _buildStationDetails() {
|
||||||
@ -190,7 +397,6 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Still get out-of-bounds keys for station limits to highlight them
|
|
||||||
final outOfBoundsKeys = _getOutOfBoundsKeys(context);
|
final outOfBoundsKeys = _getOutOfBoundsKeys(context);
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
@ -214,7 +420,7 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
|||||||
_buildDetailRow("Sampling Type:", widget.data.samplingType),
|
_buildDetailRow("Sampling Type:", widget.data.samplingType),
|
||||||
_buildDetailRow("Sample ID Code:", widget.data.sampleIdCode),
|
_buildDetailRow("Sample ID Code:", widget.data.sampleIdCode),
|
||||||
const Divider(height: 20),
|
const Divider(height: 20),
|
||||||
..._buildStationDetails(), // Use dynamic station details
|
..._buildStationDetails(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
_buildSectionCard(
|
_buildSectionCard(
|
||||||
@ -346,7 +552,7 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
|||||||
(widget.isLoading || _isHandlingSubmit)
|
(widget.isLoading || _isHandlingSubmit)
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: ElevatedButton.icon(
|
: ElevatedButton.icon(
|
||||||
onPressed: () => _handleSubmit(context), // Simplified call
|
onPressed: () => _handleSubmit(context),
|
||||||
icon: const Icon(Icons.cloud_upload),
|
icon: const Icon(Icons.cloud_upload),
|
||||||
label: const Text('Confirm & Submit'),
|
label: const Text('Confirm & Submit'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@ -420,7 +626,7 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
|||||||
isMissing ? 'N/A' : '${value.toStringAsFixed(5)} ${unit}'.trim();
|
isMissing ? 'N/A' : '${value.toStringAsFixed(5)} ${unit}'.trim();
|
||||||
final Color? defaultTextColor =
|
final Color? defaultTextColor =
|
||||||
Theme.of(context).textTheme.bodyLarge?.color;
|
Theme.of(context).textTheme.bodyLarge?.color;
|
||||||
final Color valueColor = isOutOfBounds // Still highlight if outside station limits
|
final Color valueColor = isOutOfBounds
|
||||||
? Colors.red
|
? Colors.red
|
||||||
: (isMissing ? Colors.grey : defaultTextColor ?? Colors.black);
|
: (isMissing ? Colors.grey : defaultTextColor ?? Colors.black);
|
||||||
|
|
||||||
@ -453,11 +659,34 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
|||||||
if (image != null)
|
if (image != null)
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
child: Image.file(image,
|
child: FutureBuilder<Uint8List>(
|
||||||
key: UniqueKey(),
|
key: ValueKey(image.path),
|
||||||
height: 200,
|
future: image.readAsBytes(),
|
||||||
width: double.infinity,
|
builder: (context, snapshot) {
|
||||||
fit: BoxFit.cover),
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return Container(
|
||||||
|
height: 200,
|
||||||
|
width: double.infinity,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) {
|
||||||
|
return Container(
|
||||||
|
height: 200,
|
||||||
|
width: double.infinity,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Icon(Icons.error, color: Colors.red, size: 40),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Image.memory(
|
||||||
|
snapshot.data!,
|
||||||
|
height: 200,
|
||||||
|
width: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Container(
|
Container(
|
||||||
|
|||||||
@ -94,6 +94,52 @@ class _MarineManualReportStatusLogState
|
|||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
final Map<String, bool> _isResubmitting = {};
|
final Map<String, bool> _isResubmitting = {};
|
||||||
|
|
||||||
|
// --- START: COPIED FROM SCREEN FILE ---
|
||||||
|
// This is the "single source of truth" for categories
|
||||||
|
final Map<String, List<String>> _checklistSections = {
|
||||||
|
'INTERNAL - IN-SITU SAMPLING': [ // Section title matches PDF
|
||||||
|
'Marine manual Standard Operation Procedure (SOP)', // Item text matches PDF
|
||||||
|
'Back-up Sampling Sheet & Chain of Custody form', // Item text matches PDF
|
||||||
|
'Calibration worksheet', // Item text matches PDF
|
||||||
|
'YSI EXO 2 Sonde include sensor (pH/Turbidity/Conductivity/Dissolved Oxygen)', // Item text matches PDF
|
||||||
|
'Spare set sensor (pH/Turbidity/Conductivity/Dissolved Oxygen)', // Item text matches PDF
|
||||||
|
'YSI serial cable', // Item text matches PDF
|
||||||
|
'Van Dorn Sampler (with rope & messenger)', // Item text matches PDF
|
||||||
|
'Laptop', // Item text matches PDF
|
||||||
|
'Smartphone pre-installed with application (Apps for manual sampling-MMS)', // Item text matches PDF
|
||||||
|
'GPS navigation', // Item text matches PDF
|
||||||
|
'Calibration standards (pH/Turbidity/Conductivity)', // Item text matches PDF
|
||||||
|
'Distilled water (D.I.)', // Item text matches PDF
|
||||||
|
'Universal pH indicator paper', // Item text matches PDF
|
||||||
|
'Alcohol swab', // Item text matches PDF
|
||||||
|
'Personal Floating Devices (PFD)', // Item text matches PDF
|
||||||
|
'First aid kits', // Item text matches PDF
|
||||||
|
'Disposable gloves', // Item text matches PDF
|
||||||
|
'Black plastic bags', // Item text matches PDF
|
||||||
|
'Marker pen, pen, clear tapes, brown tapes & scissors', // Item text matches PDF
|
||||||
|
'Energizer battery', // Item text matches PDF
|
||||||
|
'EXO battery opener and EXO magnet', // Item text matches PDF
|
||||||
|
'Laminated white paper', // Item text matches PDF
|
||||||
|
'Clear glass bottle (blue сар)', // Item text matches PDF
|
||||||
|
'Proper sampling attires & shoes', // Item text matches PDF
|
||||||
|
'Raincoat/Poncho', // Item text matches PDF
|
||||||
|
'Ice packets', // Item text matches PDF
|
||||||
|
],
|
||||||
|
'INTERNAL-TARBALL SAMPLING': [ // Section title matches PDF
|
||||||
|
'Measuring tape (100 meter)', // Item text matches PDF
|
||||||
|
'Steel raking', // Item text matches PDF
|
||||||
|
'Aluminum foil', // Item text matches PDF
|
||||||
|
'Zipper bags', // Item text matches PDF
|
||||||
|
],
|
||||||
|
'EXTERNAL - LABORATORY': [ // Section title matches PDF
|
||||||
|
'Sufficient sets of cooler box and sampling bottles with label', // Item text matches PDF
|
||||||
|
'Field duplicate sampling bottles (if any)', // Item text matches PDF
|
||||||
|
'Blank samples sampling bottles (if any)', // Item text matches PDF
|
||||||
|
'Preservatives (acid & alkaline)', // Item text matches PDF
|
||||||
|
],
|
||||||
|
};
|
||||||
|
// --- END: COPIED FROM SCREEN FILE ---
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -152,9 +198,7 @@ class _MarineManualReportStatusLogState
|
|||||||
tempPreSampling.add(SubmissionLogEntry(
|
tempPreSampling.add(SubmissionLogEntry(
|
||||||
type: 'Pre-Departure Checklist',
|
type: 'Pre-Departure Checklist',
|
||||||
title: 'Pre-Departure Checklist',
|
title: 'Pre-Departure Checklist',
|
||||||
// --- START: REVERTED ---
|
stationCode: 'N/A',
|
||||||
stationCode: 'N/A', // Reverted
|
|
||||||
// --- END: REVERTED ---
|
|
||||||
// --- START: MODIFIED ---
|
// --- START: MODIFIED ---
|
||||||
senderName: (log['reporterName'] as String?) ?? 'Unknown User',
|
senderName: (log['reporterName'] as String?) ?? 'Unknown User',
|
||||||
// --- END: MODIFIED ---
|
// --- END: MODIFIED ---
|
||||||
@ -317,7 +361,6 @@ class _MarineManualReportStatusLogState
|
|||||||
final data = MarineManualPreDepartureChecklistData();
|
final data = MarineManualPreDepartureChecklistData();
|
||||||
data.reporterUserId = logData['reporterUserId'];
|
data.reporterUserId = logData['reporterUserId'];
|
||||||
data.submissionDate = logData['submissionDate'];
|
data.submissionDate = logData['submissionDate'];
|
||||||
// data.location = logData['location']; // <-- REVERTED
|
|
||||||
// Reconstruct maps
|
// Reconstruct maps
|
||||||
if (logData['checklistItems'] != null) {
|
if (logData['checklistItems'] != null) {
|
||||||
data.checklistItems =
|
data.checklistItems =
|
||||||
@ -526,7 +569,7 @@ class _MarineManualReportStatusLogState
|
|||||||
_preSamplingSearchController,
|
_preSamplingSearchController,
|
||||||
),
|
),
|
||||||
_buildCategorySection(
|
_buildCategorySection(
|
||||||
'Report Log',
|
'NPE Report Log',
|
||||||
_filteredReportLogs,
|
_filteredReportLogs,
|
||||||
_reportSearchController,
|
_reportSearchController,
|
||||||
),
|
),
|
||||||
@ -732,6 +775,7 @@ class _MarineManualReportStatusLogState
|
|||||||
return value.toString();
|
return value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: ADDED WIDGET-BASED HEADER HELPER ---
|
||||||
/// Builds a formatted category header row for the data list.
|
/// Builds a formatted category header row for the data list.
|
||||||
Widget _buildCategoryHeader(BuildContext context, String title, IconData icon) {
|
Widget _buildCategoryHeader(BuildContext context, String title, IconData icon) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@ -752,60 +796,92 @@ class _MarineManualReportStatusLogState
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// --- END: ADDED WIDGET-BASED HEADER HELPER ---
|
||||||
|
|
||||||
/// Builds a formatted row for the data list.
|
// --- START: RE-INTRODUCED TABLE-BASED HELPERS ---
|
||||||
Widget _buildDataRow(String label, String? value) {
|
/// Builds a formatted category header row for the data table.
|
||||||
|
TableRow _buildCategoryRow(BuildContext context, String title, IconData icon) {
|
||||||
|
return TableRow(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16.0, bottom: 8.0, left: 8.0, right: 8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 20, color: Theme.of(context).primaryColor),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox.shrink(), // Empty cell for the second column
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a formatted row for the data dialog.
|
||||||
|
TableRow _buildDataTableRow(String label, String? value, {Color? valueColor}) {
|
||||||
String displayValue = (value == null || value.isEmpty || value == 'null') ? 'N/A' : value;
|
String displayValue = (value == null || value.isEmpty || value == 'null') ? 'N/A' : value;
|
||||||
|
|
||||||
if (displayValue == '-999.0' || displayValue == '-999') {
|
if (displayValue == '-999.0' || displayValue == '-999') {
|
||||||
displayValue = 'N/A';
|
displayValue = 'N/A';
|
||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return TableRow(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
children: [
|
||||||
child: Row(
|
Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||||
children: [
|
child: Text(
|
||||||
Expanded(
|
label,
|
||||||
flex: 2,
|
style: const TextStyle(
|
||||||
child: Text(
|
fontWeight: FontWeight.bold,
|
||||||
label,
|
fontSize: 14.0,
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 14.0,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
Expanded(
|
Padding(
|
||||||
flex: 3,
|
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
displayValue,
|
displayValue,
|
||||||
style: const TextStyle(fontSize: 14.0),
|
style: TextStyle(fontSize: 14.0, color: valueColor),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: NEW HELPER FOR PRE-DEPARTURE ---
|
/// Builds a remark row for the data dialog.
|
||||||
/// Determines the category for a given checklist item.
|
TableRow _buildRemarkTableRow(String? remark) {
|
||||||
String _getChecklistCategory(String itemName) {
|
if (remark == null || remark.isEmpty) {
|
||||||
final lowerItem = itemName.toLowerCase();
|
return const TableRow(children: [SizedBox.shrink(), SizedBox.shrink()]);
|
||||||
if (lowerItem.contains('in-situ') || lowerItem.contains('sonde') || lowerItem.contains('van dorn')) {
|
|
||||||
return 'Internal - In-Situ Sampling';
|
|
||||||
}
|
}
|
||||||
if (lowerItem.contains('tarball')) {
|
return TableRow(
|
||||||
return 'Internal - Tarball Sampling';
|
children: [
|
||||||
}
|
const SizedBox.shrink(), // Empty cell for the label
|
||||||
if (lowerItem.contains('laboratory') || lowerItem.contains('ice') || lowerItem.contains('bottle')) {
|
Padding(
|
||||||
return 'External - Laboratory';
|
padding: const EdgeInsets.only(bottom: 8.0, left: 8.0, right: 8.0),
|
||||||
}
|
child: Text(
|
||||||
// Add more rules here if needed
|
"Remark: $remark",
|
||||||
return 'General';
|
style: TextStyle(
|
||||||
|
fontSize: 13.0,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// --- END: NEW HELPER FOR PRE-DEPARTURE ---
|
// --- END: RE-INTRODUCED TABLE-BASED HELPERS ---
|
||||||
|
|
||||||
/// Shows the categorized and formatted data log in a dialog
|
/// Shows the categorized and formatted data log in a dialog
|
||||||
void _showDataDialog(BuildContext context, SubmissionLogEntry log) {
|
void _showDataDialog(BuildContext context, SubmissionLogEntry log) {
|
||||||
@ -813,118 +889,152 @@ class _MarineManualReportStatusLogState
|
|||||||
Widget dialogContent; // This will hold either a ListView or a Column
|
Widget dialogContent; // This will hold either a ListView or a Column
|
||||||
|
|
||||||
if (log.type == 'Pre-Departure Checklist') {
|
if (log.type == 'Pre-Departure Checklist') {
|
||||||
// --- START: Handle Pre-Departure Checklist (uses ListView) ---
|
// --- START: Handle Pre-Departure Checklist (uses Column/ListView) ---
|
||||||
final items = Map<String, bool>.from(data['checklistItems'] ?? {});
|
final items = Map<String, bool>.from(data['checklistItems'] ?? {});
|
||||||
final remarks = Map<String, String>.from(data['remarks'] ?? {});
|
final remarks = Map<String, String>.from(data['remarks'] ?? {});
|
||||||
|
|
||||||
// 1. Group items by category
|
// 1. Build the list of widgets
|
||||||
final Map<String, List<MapEntry<String, bool>>> categorizedItems = {};
|
|
||||||
for (final entry in items.entries) {
|
|
||||||
final category = _getChecklistCategory(entry.key);
|
|
||||||
if (!categorizedItems.containsKey(category)) {
|
|
||||||
categorizedItems[category] = [];
|
|
||||||
}
|
|
||||||
categorizedItems[category]!.add(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Define the order
|
|
||||||
const categoryOrder = [
|
|
||||||
'Internal - In-Situ Sampling',
|
|
||||||
'Internal - Tarball Sampling',
|
|
||||||
'External - Laboratory',
|
|
||||||
'General'
|
|
||||||
];
|
|
||||||
|
|
||||||
// 3. Build the list of widgets
|
|
||||||
final List<Widget> contentWidgets = [];
|
final List<Widget> contentWidgets = [];
|
||||||
for (final category in categoryOrder) {
|
|
||||||
if (categorizedItems.containsKey(category) && categorizedItems[category]!.isNotEmpty) {
|
|
||||||
// Add the category header
|
|
||||||
contentWidgets.add(_buildCategoryHeader(context, category, Icons.check_box_outlined));
|
|
||||||
|
|
||||||
// Add the items for that category
|
// 2. Iterate over the DEFINED categories from the map
|
||||||
contentWidgets.addAll(
|
for (final categoryEntry in _checklistSections.entries) {
|
||||||
categorizedItems[category]!.map((entry) {
|
final categoryTitle = categoryEntry.key;
|
||||||
final key = entry.key;
|
final categoryItems = categoryEntry.value;
|
||||||
final value = entry.value;
|
|
||||||
final status = value ? 'Yes' : 'No';
|
|
||||||
final remark = remarks[key] ?? '';
|
|
||||||
|
|
||||||
return Padding(
|
// Add the category header
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
contentWidgets.add(_buildCategoryHeader(context, categoryTitle, Icons.check_box_outlined));
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
// 3. Add the items for that category
|
||||||
children: [
|
contentWidgets.addAll(
|
||||||
// Row 1: Item and Status
|
categoryItems.map((itemName) {
|
||||||
Row(
|
// Find the item's status and remark from the log data
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
final bool value = items[itemName] ?? false; // Default to 'No'
|
||||||
children: [
|
final String remark = remarks[itemName] ?? '';
|
||||||
// Item Name
|
final String status = value ? 'Yes' : 'No';
|
||||||
Expanded(
|
|
||||||
flex: 3,
|
return Padding(
|
||||||
child: Text(
|
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0),
|
||||||
key,
|
child: Column(
|
||||||
style: const TextStyle(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
fontWeight: FontWeight.bold,
|
children: [
|
||||||
fontSize: 14.0,
|
// Row 1: Item and Status
|
||||||
),
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Item Name
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: Text(
|
||||||
|
itemName, // Use the name from the category list
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
// Status
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
// Status
|
||||||
flex: 1,
|
Expanded(
|
||||||
child: Text(
|
flex: 1,
|
||||||
status,
|
child: Text(
|
||||||
|
status,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14.0,
|
||||||
|
color: value ? Colors.green.shade700 : Colors.red.shade700,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// Row 2: Remark (only if it exists)
|
||||||
|
if (remark.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 6.0, left: 8.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Remark: ",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.0,
|
fontSize: 13.0,
|
||||||
color: value ? Colors.green.shade700 : Colors.red.shade700,
|
color: Colors.grey.shade700,
|
||||||
fontWeight: FontWeight.bold,
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.end,
|
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
],
|
child: Text(
|
||||||
),
|
remark,
|
||||||
// Row 2: Remark (only if it exists)
|
|
||||||
if (remark.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 6.0, left: 8.0),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Remark: ",
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13.0,
|
fontSize: 13.0,
|
||||||
color: Colors.grey.shade700,
|
color: Colors.grey.shade700,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: Text(
|
],
|
||||||
remark,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13.0,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
),
|
||||||
}).toList()
|
);
|
||||||
);
|
}).toList()
|
||||||
|
);
|
||||||
|
|
||||||
// Add a divider after the category
|
// Add a divider after the category
|
||||||
contentWidgets.add(const Divider(height: 16));
|
contentWidgets.add(const Divider(height: 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Handle any items that were in the log but NOT in the category map
|
||||||
|
final Set<String> allCategorizedItems = _checklistSections.values.expand((list) => list).toSet();
|
||||||
|
final List<Widget> otherItems = [];
|
||||||
|
|
||||||
|
for (final itemEntry in items.entries) {
|
||||||
|
if (!allCategorizedItems.contains(itemEntry.key)) {
|
||||||
|
// This item was not in our hard-coded map
|
||||||
|
final key = itemEntry.key;
|
||||||
|
final value = itemEntry.value;
|
||||||
|
final status = value ? 'Yes' : 'No';
|
||||||
|
final remark = remarks[key] ?? '';
|
||||||
|
|
||||||
|
otherItems.add(
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(flex: 3, child: Text(key, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14.0))),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(flex: 1, child: Text(status, style: TextStyle(fontSize: 14.0, color: value ? Colors.green.shade700 : Colors.red.shade700, fontWeight: FontWeight.bold), textAlign: TextAlign.end)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (remark.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 6.0, left: 8.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Remark: ", style: TextStyle(fontSize: 13.0, color: Colors.grey.shade700, fontStyle: FontStyle.italic)),
|
||||||
|
Expanded(child: Text(remark, style: TextStyle(fontSize: 13.0, color: Colors.grey.shade700, fontStyle: FontStyle.italic))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (otherItems.isNotEmpty) {
|
||||||
|
contentWidgets.add(_buildCategoryHeader(context, "Other Items", Icons.help_outline));
|
||||||
|
contentWidgets.addAll(otherItems);
|
||||||
|
}
|
||||||
|
|
||||||
if (contentWidgets.isEmpty) {
|
if (contentWidgets.isEmpty) {
|
||||||
dialogContent = const Center(child: Text('No checklist items found.'));
|
dialogContent = const Center(child: Text('No checklist items found.'));
|
||||||
} else {
|
} else {
|
||||||
@ -937,25 +1047,20 @@ class _MarineManualReportStatusLogState
|
|||||||
// --- END: Handle Pre-Departure Checklist ---
|
// --- END: Handle Pre-Departure Checklist ---
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// --- START: Handle ALL OTHER Log Types (uses Column) ---
|
// --- START: Handle ALL OTHER Log Types (uses Table) ---
|
||||||
final List<Widget> contentWidgets = [];
|
final List<TableRow> tableRows = [];
|
||||||
|
|
||||||
// --- Helper for nested maps ---
|
// --- Helper for nested maps ---
|
||||||
void addNestedMapRows(Map<String, dynamic> map) {
|
void addNestedMapRows(Map<String, dynamic> map) {
|
||||||
map.forEach((key, value) {
|
map.forEach((key, value) {
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
// Handle nested maps (e.g., ysiSensorChecks)
|
// Handle nested maps (e.g., ysiSensorChecks)
|
||||||
contentWidgets.add(_buildDataRow(key, ''));
|
tableRows.add(_buildDataTableRow(key, ''));
|
||||||
value.forEach((subKey, subValue) {
|
value.forEach((subKey, subValue) {
|
||||||
contentWidgets.add(
|
tableRows.add(_buildDataTableRow(' $subKey', subValue?.toString() ?? 'N/A'));
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
|
||||||
child: _buildDataRow(subKey, subValue?.toString() ?? 'N/A'),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
contentWidgets.add(_buildDataRow(key, value?.toString() ?? 'N/A'));
|
tableRows.add(_buildDataTableRow(key, value?.toString() ?? 'N/A'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -963,119 +1068,128 @@ class _MarineManualReportStatusLogState
|
|||||||
|
|
||||||
switch (log.type) {
|
switch (log.type) {
|
||||||
case 'Sonde Calibration':
|
case 'Sonde Calibration':
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'Sonde Info', Icons.info_outline));
|
tableRows.add(_buildCategoryRow(context, 'Sonde Info', Icons.info_outline));
|
||||||
contentWidgets.add(_buildDataRow('Sonde Serial #', _getString(data, 'sondeSerialNumber')));
|
tableRows.add(_buildDataTableRow('Sonde Serial #', _getString(data, 'sondeSerialNumber')));
|
||||||
contentWidgets.add(_buildDataRow('Firmware Version', _getString(data, 'firmwareVersion')));
|
tableRows.add(_buildDataTableRow('Firmware Version', _getString(data, 'firmwareVersion')));
|
||||||
contentWidgets.add(_buildDataRow('KOR Version', _getString(data, 'korVersion')));
|
tableRows.add(_buildDataTableRow('KOR Version', _getString(data, 'korVersion')));
|
||||||
contentWidgets.add(_buildDataRow('Location', _getString(data, 'location')));
|
tableRows.add(_buildDataTableRow('Location', _getString(data, 'location')));
|
||||||
contentWidgets.add(_buildDataRow('Start Time', _getString(data, 'startDateTime')));
|
tableRows.add(_buildDataTableRow('Start Time', _getString(data, 'startDateTime')));
|
||||||
contentWidgets.add(_buildDataRow('End Time', _getString(data, 'endDateTime')));
|
tableRows.add(_buildDataTableRow('End Time', _getString(data, 'endDateTime')));
|
||||||
contentWidgets.add(_buildDataRow('Status', _getString(data, 'calibration_status')));
|
tableRows.add(_buildDataTableRow('Status', _getString(data, 'calibration_status')));
|
||||||
contentWidgets.add(_buildDataRow('Remarks', _getString(data, 'remarks')));
|
tableRows.add(_buildDataTableRow('Remarks', _getString(data, 'remarks')));
|
||||||
|
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'pH 7.0', Icons.science_outlined));
|
tableRows.add(_buildCategoryRow(context, 'pH 7.0', Icons.science_outlined));
|
||||||
contentWidgets.add(_buildDataRow('MV', _getString(data, 'ph_7_mv')));
|
tableRows.add(_buildDataTableRow('MV', _getString(data, 'ph_7_mv')));
|
||||||
contentWidgets.add(_buildDataRow('Before', _getString(data, 'ph_7_before')));
|
tableRows.add(_buildDataTableRow('Before', _getString(data, 'ph_7_before')));
|
||||||
contentWidgets.add(_buildDataRow('After', _getString(data, 'ph_7_after')));
|
tableRows.add(_buildDataTableRow('After', _getString(data, 'ph_7_after')));
|
||||||
|
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'pH 10.0', Icons.science_outlined));
|
tableRows.add(_buildCategoryRow(context, 'pH 10.0', Icons.science_outlined));
|
||||||
contentWidgets.add(_buildDataRow('MV', _getString(data, 'ph_10_mv')));
|
tableRows.add(_buildDataTableRow('MV', _getString(data, 'ph_10_mv')));
|
||||||
contentWidgets.add(_buildDataRow('Before', _getString(data, 'ph_10_before')));
|
tableRows.add(_buildDataTableRow('Before', _getString(data, 'ph_10_before')));
|
||||||
contentWidgets.add(_buildDataRow('After', _getString(data, 'ph_10_after')));
|
tableRows.add(_buildDataTableRow('After', _getString(data, 'ph_10_after')));
|
||||||
|
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'Conductivity', Icons.thermostat));
|
tableRows.add(_buildCategoryRow(context, 'Conductivity', Icons.thermostat));
|
||||||
contentWidgets.add(_buildDataRow('Before', _getString(data, 'cond_before')));
|
tableRows.add(_buildDataTableRow('Before', _getString(data, 'cond_before')));
|
||||||
contentWidgets.add(_buildDataRow('After', _getString(data, 'cond_after')));
|
tableRows.add(_buildDataTableRow('After', _getString(data, 'cond_after')));
|
||||||
|
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'Dissolved Oxygen', Icons.air));
|
tableRows.add(_buildCategoryRow(context, 'Dissolved Oxygen', Icons.air));
|
||||||
contentWidgets.add(_buildDataRow('Before', _getString(data, 'do_before')));
|
tableRows.add(_buildDataTableRow('Before', _getString(data, 'do_before')));
|
||||||
contentWidgets.add(_buildDataRow('After', _getString(data, 'do_after')));
|
tableRows.add(_buildDataTableRow('After', _getString(data, 'do_after')));
|
||||||
|
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'Turbidity', Icons.waves));
|
tableRows.add(_buildCategoryRow(context, 'Turbidity', Icons.waves));
|
||||||
contentWidgets.add(_buildDataRow('0 NTU Before', _getString(data, 'turbidity_0_before')));
|
tableRows.add(_buildDataTableRow('0 NTU Before', _getString(data, 'turbidity_0_before')));
|
||||||
contentWidgets.add(_buildDataRow('0 NTU After', _getString(data, 'turbidity_0_after')));
|
tableRows.add(_buildDataTableRow('0 NTU After', _getString(data, 'turbidity_0_after')));
|
||||||
contentWidgets.add(_buildDataRow('124 NTU Before', _getString(data, 'turbidity_124_before')));
|
tableRows.add(_buildDataTableRow('124 NTU Before', _getString(data, 'turbidity_124_before')));
|
||||||
contentWidgets.add(_buildDataRow('124 NTU After', _getString(data, 'turbidity_124_after')));
|
tableRows.add(_buildDataTableRow('124 NTU After', _getString(data, 'turbidity_124_after')));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Equipment Maintenance':
|
case 'Equipment Maintenance':
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'YSI Sonde Checks', Icons.build_circle_outlined));
|
tableRows.add(_buildCategoryRow(context, 'YSI Sonde Checks', Icons.build_circle_outlined));
|
||||||
if (data['ysiSondeChecks'] != null) {
|
if (data['ysiSondeChecks'] != null) {
|
||||||
addNestedMapRows(Map<String, dynamic>.from(data['ysiSondeChecks']));
|
addNestedMapRows(Map<String, dynamic>.from(data['ysiSondeChecks']));
|
||||||
}
|
}
|
||||||
contentWidgets.add(_buildDataRow('Comments', _getString(data, 'ysiSondeComments')));
|
tableRows.add(_buildDataTableRow('Comments', _getString(data, 'ysiSondeComments')));
|
||||||
|
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'YSI Sensor Checks', Icons.sensors));
|
tableRows.add(_buildCategoryRow(context, 'YSI Sensor Checks', Icons.sensors));
|
||||||
if (data['ysiSensorChecks'] != null) {
|
if (data['ysiSensorChecks'] != null) {
|
||||||
addNestedMapRows(Map<String, dynamic>.from(data['ysiSensorChecks']));
|
addNestedMapRows(Map<String, dynamic>.from(data['ysiSensorChecks']));
|
||||||
}
|
}
|
||||||
contentWidgets.add(_buildDataRow('Comments', _getString(data, 'ysiSensorComments')));
|
tableRows.add(_buildDataTableRow('Comments', _getString(data, 'ysiSensorComments')));
|
||||||
|
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'YSI Replacements', Icons.published_with_changes));
|
tableRows.add(_buildCategoryRow(context, 'YSI Replacements', Icons.published_with_changes));
|
||||||
if (data['ysiReplacements'] != null) {
|
if (data['ysiReplacements'] != null) {
|
||||||
addNestedMapRows(Map<String, dynamic>.from(data['ysiReplacements']));
|
addNestedMapRows(Map<String, dynamic>.from(data['ysiReplacements']));
|
||||||
}
|
}
|
||||||
|
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'Van Dorn Checks', Icons.opacity));
|
tableRows.add(_buildCategoryRow(context, 'Van Dorn Checks', Icons.opacity));
|
||||||
if (data['vanDornChecks'] != null) {
|
if (data['vanDornChecks'] != null) {
|
||||||
addNestedMapRows(Map<String, dynamic>.from(data['vanDornChecks']));
|
addNestedMapRows(Map<String, dynamic>.from(data['vanDornChecks']));
|
||||||
}
|
}
|
||||||
contentWidgets.add(_buildDataRow('Comments', _getString(data, 'vanDornComments')));
|
tableRows.add(_buildDataTableRow('Comments', _getString(data, 'vanDornComments')));
|
||||||
contentWidgets.add(_buildDataRow('Current Serial', _getString(data, 'vanDornCurrentSerial')));
|
tableRows.add(_buildDataTableRow('Current Serial', _getString(data, 'vanDornCurrentSerial')));
|
||||||
contentWidgets.add(_buildDataRow('New Serial', _getString(data, 'vanDornNewSerial')));
|
tableRows.add(_buildDataTableRow('New Serial', _getString(data, 'vanDornNewSerial')));
|
||||||
|
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'Van Dorn Replacements', Icons.published_with_changes));
|
tableRows.add(_buildCategoryRow(context, 'Van Dorn Replacements', Icons.published_with_changes));
|
||||||
if (data['vanDornReplacements'] != null) {
|
if (data['vanDornReplacements'] != null) {
|
||||||
addNestedMapRows(Map<String, dynamic>.from(data['vanDornReplacements']));
|
addNestedMapRows(Map<String, dynamic>.from(data['vanDornReplacements']));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'NPE Report':
|
case 'NPE Report':
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'Event Info', Icons.calendar_today));
|
tableRows.add(_buildCategoryRow(context, 'Event Info', Icons.calendar_today));
|
||||||
contentWidgets.add(_buildDataRow('Date', _getString(data, 'eventDate')));
|
tableRows.add(_buildDataTableRow('Date', _getString(data, 'eventDate')));
|
||||||
contentWidgets.add(_buildDataRow('Time', _getString(data, 'eventTime')));
|
tableRows.add(_buildDataTableRow('Time', _getString(data, 'eventTime')));
|
||||||
contentWidgets.add(_buildDataRow('Sampler', _getString(data, 'firstSamplerName')));
|
tableRows.add(_buildDataTableRow('Sampler', _getString(data, 'firstSamplerName')));
|
||||||
|
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'Location', Icons.location_on_outlined));
|
tableRows.add(_buildCategoryRow(context, 'Location', Icons.location_on_outlined));
|
||||||
if (data['selectedStation'] != null) {
|
if (data['selectedStation'] != null) {
|
||||||
contentWidgets.add(_buildDataRow('Station', _getString(data['selectedStation'], 'man_station_name') ?? _getString(data['selectedStation'], 'tbl_station_name')));
|
tableRows.add(_buildDataTableRow('Station', _getString(data['selectedStation'], 'man_station_name') ?? _getString(data['selectedStation'], 'tbl_station_name')));
|
||||||
} else {
|
} else {
|
||||||
contentWidgets.add(_buildDataRow('Location', _getString(data, 'locationDescription')));
|
tableRows.add(_buildDataTableRow('Location', _getString(data, 'locationDescription')));
|
||||||
contentWidgets.add(_buildDataRow('State', _getString(data, 'stateName')));
|
tableRows.add(_buildDataTableRow('State', _getString(data, 'stateName')));
|
||||||
}
|
}
|
||||||
contentWidgets.add(_buildDataRow('Latitude', _getString(data, 'latitude')));
|
tableRows.add(_buildDataTableRow('Latitude', _getString(data, 'latitude')));
|
||||||
contentWidgets.add(_buildDataRow('Longitude', _getString(data, 'longitude')));
|
tableRows.add(_buildDataTableRow('Longitude', _getString(data, 'longitude')));
|
||||||
|
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'Parameters', Icons.bar_chart));
|
tableRows.add(_buildCategoryRow(context, 'Parameters', Icons.bar_chart));
|
||||||
contentWidgets.add(_buildDataRow('Oxygen Conc (mg/L)', _getString(data, 'oxygenConcentration')));
|
tableRows.add(_buildDataTableRow('Oxygen Conc (mg/L)', _getString(data, 'oxygenConcentration')));
|
||||||
contentWidgets.add(_buildDataRow('Oxygen Sat (%)', _getString(data, 'oxygenSaturation')));
|
tableRows.add(_buildDataTableRow('Oxygen Sat (%)', _getString(data, 'oxygenSaturation')));
|
||||||
contentWidgets.add(_buildDataRow('pH', _getString(data, 'ph')));
|
tableRows.add(_buildDataTableRow('pH', _getString(data, 'ph')));
|
||||||
contentWidgets.add(_buildDataRow('Conductivity (µS/cm)', _getString(data, 'electricalConductivity')));
|
tableRows.add(_buildDataTableRow('Conductivity (µS/cm)', _getString(data, 'electricalConductivity')));
|
||||||
contentWidgets.add(_buildDataRow('Temperature (°C)', _getString(data, 'temperature')));
|
tableRows.add(_buildDataTableRow('Temperature (°C)', _getString(data, 'temperature')));
|
||||||
contentWidgets.add(_buildDataRow('Turbidity (NTU)', _getString(data, 'turbidity')));
|
tableRows.add(_buildDataTableRow('Turbidity (NTU)', _getString(data, 'turbidity')));
|
||||||
|
|
||||||
contentWidgets.add(_buildCategoryHeader(context, 'Observations', Icons.warning_amber_rounded));
|
tableRows.add(_buildCategoryRow(context, 'Observations', Icons.warning_amber_rounded));
|
||||||
if (data['fieldObservations'] != null) {
|
if (data['fieldObservations'] != null) {
|
||||||
final observations = Map<String, bool>.from(data['fieldObservations']);
|
final observations = Map<String, bool>.from(data['fieldObservations']);
|
||||||
observations.forEach((key, value) {
|
observations.forEach((key, value) {
|
||||||
if(value) contentWidgets.add(_buildDataRow(key, 'Checked'));
|
if(value) tableRows.add(_buildDataTableRow(key, 'Checked'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
contentWidgets.add(_buildDataRow('Other Remarks', _getString(data, 'othersObservationRemark')));
|
tableRows.add(_buildDataTableRow('Other Remarks', _getString(data, 'othersObservationRemark')));
|
||||||
contentWidgets.add(_buildDataRow('Possible Source', _getString(data, 'possibleSource')));
|
tableRows.add(_buildDataTableRow('Possible Source', _getString(data, 'possibleSource')));
|
||||||
if (data['selectedTarballClassification'] != null) {
|
if (data['selectedTarballClassification'] != null) {
|
||||||
contentWidgets.add(_buildDataRow('Tarball Class', _getString(data['selectedTarballClassification'], 'classification_name')));
|
tableRows.add(_buildDataTableRow('Tarball Class', _getString(data['selectedTarballClassification'], 'classification_name')));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
contentWidgets.add(_buildDataRow('Error', 'No data view configured for log type: ${log.type}'));
|
tableRows.add(_buildDataTableRow('Error', 'No data view configured for log type: ${log.type}'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign the Column as the content for the 'else' block
|
// Assign the Table as the content for the 'else' block
|
||||||
dialogContent = Column(
|
dialogContent = Table(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
columnWidths: const {
|
||||||
children: contentWidgets,
|
0: IntrinsicColumnWidth(), // Label column
|
||||||
|
1: FlexColumnWidth(), // Value column
|
||||||
|
},
|
||||||
|
border: TableBorder(
|
||||||
|
horizontalInside: BorderSide(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
children: tableRows,
|
||||||
);
|
);
|
||||||
// --- END: Handle ALL OTHER Log Types ---
|
// --- END: Handle ALL OTHER Log Types ---
|
||||||
}
|
}
|
||||||
|
|||||||
@ -409,7 +409,7 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
|
|
||||||
List<Map<String, dynamic>> _validateParameters(Map<String, double> readings, List<Map<String, dynamic>> limits) {
|
List<Map<String, dynamic>> _validateParameters(Map<String, double> readings, List<Map<String, dynamic>> limits) {
|
||||||
final List<Map<String, dynamic>> invalidParams = [];
|
final List<Map<String, dynamic>> invalidParams = [];
|
||||||
final int? stationId = widget.data.selectedStation?['station_id'];
|
final int? stationId = widget.data.selectedStation?['man_station_id'];
|
||||||
|
|
||||||
debugPrint("--- Parameter Validation Start ---");
|
debugPrint("--- Parameter Validation Start ---");
|
||||||
debugPrint("Selected Station ID: $stationId");
|
debugPrint("Selected Station ID: $stationId");
|
||||||
|
|||||||
@ -47,7 +47,7 @@ class _InSituStep4SummaryState extends State<InSituStep4Summary> {
|
|||||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||||
final marineLimits = authProvider.marineParameterLimits ?? [];
|
final marineLimits = authProvider.marineParameterLimits ?? [];
|
||||||
final Set<String> invalidKeys = {};
|
final Set<String> invalidKeys = {};
|
||||||
final int? stationId = widget.data.selectedStation?['station_id'];
|
final int? stationId = widget.data.selectedStation?['man_station_id'];
|
||||||
|
|
||||||
final readings = {
|
final readings = {
|
||||||
'oxygenConcentration': widget.data.oxygenConcentration,
|
'oxygenConcentration': widget.data.oxygenConcentration,
|
||||||
|
|||||||
@ -338,7 +338,11 @@ class LocalStorageService {
|
|||||||
await eventDir.create(recursive: true);
|
await eventDir.create(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, dynamic> jsonData = data.toDbJson();
|
// --- START: MODIFICATION (FIXED ERROR) ---
|
||||||
|
// Changed data.toDbJson() to data.toMap() to get a Map, not a String.
|
||||||
|
final Map<String, dynamic> jsonData = data.toMap();
|
||||||
|
// --- END: MODIFICATION (FIXED ERROR) ---
|
||||||
|
|
||||||
jsonData['submissionStatus'] = data.submissionStatus;
|
jsonData['submissionStatus'] = data.submissionStatus;
|
||||||
jsonData['submissionMessage'] = data.submissionMessage;
|
jsonData['submissionMessage'] = data.submissionMessage;
|
||||||
|
|
||||||
|
|||||||
@ -291,11 +291,19 @@ class MarineInSituSamplingService {
|
|||||||
// --- START FIX: Add ftpConfigId when queuing ---
|
// --- START FIX: Add ftpConfigId when queuing ---
|
||||||
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
||||||
|
|
||||||
|
// --- MODIFIED: Use new data model methods for multi-json zip ---
|
||||||
final dataZip = await _zippingService.createDataZip(
|
final dataZip = await _zippingService.createDataZip(
|
||||||
jsonDataMap: {'db.json': jsonEncode(data.toDbJson())},
|
jsonDataMap: {
|
||||||
|
'db.json': data.toDbJson(),
|
||||||
|
'marine_insitu_basic_form.json': data.toBasicFormJson(),
|
||||||
|
'marine_sampling_reading.json': data.toReadingJson(),
|
||||||
|
'marine_manual_info.json': data.toManualInfoJson(),
|
||||||
|
},
|
||||||
baseFileName: baseFileNameForQueue,
|
baseFileName: baseFileNameForQueue,
|
||||||
destinationDir: null, // Use temp dir
|
destinationDir: null, // Use temp dir
|
||||||
);
|
);
|
||||||
|
// --- END MODIFIED ---
|
||||||
|
|
||||||
if (dataZip != null) {
|
if (dataZip != null) {
|
||||||
// Queue for each config separately
|
// Queue for each config separately
|
||||||
for (final config in ftpConfigs) {
|
for (final config in ftpConfigs) {
|
||||||
@ -401,7 +409,10 @@ class MarineInSituSamplingService {
|
|||||||
// Save/Update local log first
|
// Save/Update local log first
|
||||||
if (savedLogPath != null && savedLogPath.isNotEmpty) {
|
if (savedLogPath != null && savedLogPath.isNotEmpty) {
|
||||||
// Need to reconstruct the map with file paths for updating
|
// Need to reconstruct the map with file paths for updating
|
||||||
Map<String, dynamic> logUpdateData = data.toDbJson();
|
// --- START: MODIFICATION (FIXED ERROR) ---
|
||||||
|
// Changed data.toDbJson() to data.toMap() to get a Map, not a String.
|
||||||
|
Map<String, dynamic> logUpdateData = data.toMap();
|
||||||
|
// --- END: MODIFICATION (FIXED ERROR) ---
|
||||||
final imageFiles = data.toApiImageFiles();
|
final imageFiles = data.toApiImageFiles();
|
||||||
imageFiles.forEach((key, file) {
|
imageFiles.forEach((key, file) {
|
||||||
logUpdateData[key] = file?.path; // Add paths back
|
logUpdateData[key] = file?.path; // Add paths back
|
||||||
@ -439,12 +450,21 @@ class MarineInSituSamplingService {
|
|||||||
return {'success': true, 'message': successMessage, 'reportId': null}; // No report ID yet
|
return {'success': true, 'message': successMessage, 'reportId': null}; // No report ID yet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: MODIFIED _generateBaseFileName ---
|
||||||
/// Helper to generate the base filename for ZIP files.
|
/// Helper to generate the base filename for ZIP files.
|
||||||
String _generateBaseFileName(InSituSamplingData data) {
|
String _generateBaseFileName(InSituSamplingData data) {
|
||||||
final stationCode = data.selectedStation?['man_station_code'] ?? 'NA';
|
final stationCode = data.selectedStation?['man_station_code'] ?? 'NA';
|
||||||
final fileTimestamp = "${data.samplingDate}_${data.samplingTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
|
||||||
return '${stationCode}_$fileTimestamp';
|
// Check if reportId (timestamp) is available.
|
||||||
|
if (data.reportId != null && data.reportId!.isNotEmpty) {
|
||||||
|
return '${stationCode}_${data.reportId}';
|
||||||
|
} else {
|
||||||
|
// Fallback to old method if reportId is not available (e.g., offline queue)
|
||||||
|
final fileTimestamp = "${data.samplingDate}_${data.samplingTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
||||||
|
return '${stationCode}_$fileTimestamp';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED _generateBaseFileName ---
|
||||||
|
|
||||||
/// Generates data and image ZIP files and uploads them using SubmissionFtpService.
|
/// Generates data and image ZIP files and uploads them using SubmissionFtpService.
|
||||||
Future<Map<String, dynamic>> _generateAndUploadFtpFiles(InSituSamplingData data, Map<String, File> imageFiles, String serverName, String moduleName) async {
|
Future<Map<String, dynamic>> _generateAndUploadFtpFiles(InSituSamplingData data, Map<String, File> imageFiles, String serverName, String moduleName) async {
|
||||||
@ -455,19 +475,32 @@ class MarineInSituSamplingService {
|
|||||||
module: 'marine',
|
module: 'marine',
|
||||||
subModule: 'marine_in_situ_sampling', // Correct sub-module path
|
subModule: 'marine_in_situ_sampling', // Correct sub-module path
|
||||||
);
|
);
|
||||||
final folderName = data.reportId ?? baseFileName;
|
|
||||||
|
// --- START: MODIFIED folderName ---
|
||||||
|
// Use baseFileName for the folder name to match [stationCode]_[reportId]
|
||||||
|
final folderName = baseFileName;
|
||||||
|
// --- END: MODIFIED folderName ---
|
||||||
|
|
||||||
final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, folderName)) : null;
|
final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, folderName)) : null;
|
||||||
|
|
||||||
if (localSubmissionDir != null && !await localSubmissionDir.exists()) {
|
if (localSubmissionDir != null && !await localSubmissionDir.exists()) {
|
||||||
await localSubmissionDir.create(recursive: true);
|
await localSubmissionDir.create(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and upload data ZIP
|
// --- START: MODIFIED createDataZip CALL ---
|
||||||
|
// Create and upload data ZIP (with multiple JSON files as per new requirement)
|
||||||
final dataZip = await _zippingService.createDataZip(
|
final dataZip = await _zippingService.createDataZip(
|
||||||
jsonDataMap: {'db.json': jsonEncode(data.toDbJson())}, // Use toDbJson for FTP
|
jsonDataMap: {
|
||||||
|
'db.json': data.toDbJson(),
|
||||||
|
'marine_insitu_basic_form.json': data.toBasicFormJson(),
|
||||||
|
'marine_sampling_reading.json': data.toReadingJson(),
|
||||||
|
'marine_manual_info.json': data.toManualInfoJson(),
|
||||||
|
},
|
||||||
baseFileName: baseFileName,
|
baseFileName: baseFileName,
|
||||||
destinationDir: localSubmissionDir,
|
destinationDir: localSubmissionDir,
|
||||||
);
|
);
|
||||||
|
// --- END: MODIFIED createDataZip CALL ---
|
||||||
|
|
||||||
Map<String, dynamic> ftpDataResult = {'success': true, 'statuses': []};
|
Map<String, dynamic> ftpDataResult = {'success': true, 'statuses': []};
|
||||||
if (dataZip != null) {
|
if (dataZip != null) {
|
||||||
ftpDataResult = await _submissionFtpService.submit(
|
ftpDataResult = await _submissionFtpService.submit(
|
||||||
@ -516,7 +549,10 @@ class MarineInSituSamplingService {
|
|||||||
final baseFileName = _generateBaseFileName(data); // Use helper
|
final baseFileName = _generateBaseFileName(data); // Use helper
|
||||||
|
|
||||||
// Prepare log data map including file paths
|
// Prepare log data map including file paths
|
||||||
Map<String, dynamic> logMapData = data.toDbJson();
|
// --- START: MODIFICATION (FIXED ERROR) ---
|
||||||
|
// Changed data.toDbJson() to data.toMap() to get a Map, not a String.
|
||||||
|
Map<String, dynamic> logMapData = data.toMap();
|
||||||
|
// --- END: MODIFICATION (FIXED ERROR) ---
|
||||||
final imageFileMap = data.toApiImageFiles();
|
final imageFileMap = data.toApiImageFiles();
|
||||||
imageFileMap.forEach((key, file) {
|
imageFileMap.forEach((key, file) {
|
||||||
logMapData[key] = file?.path; // Store path or null
|
logMapData[key] = file?.path; // Store path or null
|
||||||
@ -650,7 +686,9 @@ class MarineInSituSamplingService {
|
|||||||
final allLimits = await _dbHelper.loadMarineParameterLimits() ?? [];
|
final allLimits = await _dbHelper.loadMarineParameterLimits() ?? [];
|
||||||
if (allLimits.isEmpty) return "";
|
if (allLimits.isEmpty) return "";
|
||||||
|
|
||||||
final int? stationId = data.selectedStation?['station_id'];
|
// --- START FIX: Use correct key 'man_station_id' ---
|
||||||
|
final dynamic stationId = data.selectedStation?['man_station_id'];
|
||||||
|
// --- END FIX ---
|
||||||
if (stationId == null) return ""; // Cannot check limits without a station ID
|
if (stationId == null) return ""; // Cannot check limits without a station ID
|
||||||
|
|
||||||
final readings = {
|
final readings = {
|
||||||
@ -771,7 +809,7 @@ class MarineInSituSamplingService {
|
|||||||
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/A';
|
final upperStr = upperLimit?.toStringAsFixed(5) ?? 'N/á';
|
||||||
String limitStr;
|
String limitStr;
|
||||||
if (lowerStr != 'N/A' && upperStr != 'N/A') {
|
if (lowerStr != 'N/A' && upperStr != 'N/A') {
|
||||||
limitStr = '$lowerStr - $upperStr';
|
limitStr = '$lowerStr - $upperStr';
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import 'package:permission_handler/permission_handler.dart';
|
|||||||
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
||||||
import 'package:usb_serial/usb_serial.dart';
|
import 'package:usb_serial/usb_serial.dart';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
|
import 'package:intl/intl.dart'; // Import intl
|
||||||
|
|
||||||
import '../auth_provider.dart';
|
import '../auth_provider.dart';
|
||||||
import 'location_service.dart';
|
import 'location_service.dart';
|
||||||
@ -71,9 +72,10 @@ class MarineInvestigativeSamplingService {
|
|||||||
img.Image? originalImage = img.decodeImage(bytes);
|
img.Image? originalImage = img.decodeImage(bytes);
|
||||||
if (originalImage == null) return null;
|
if (originalImage == null) return null;
|
||||||
|
|
||||||
|
// --- MODIFIED: Enforce landscape check logic from in-situ ---
|
||||||
if (isRequired && originalImage.height > originalImage.width) {
|
if (isRequired && originalImage.height > originalImage.width) {
|
||||||
debugPrint("Image rejected: Must be in landscape orientation.");
|
debugPrint("Image rejected: Must be in landscape orientation.");
|
||||||
return null;
|
return null; // Return null if landscape is required and check fails
|
||||||
}
|
}
|
||||||
|
|
||||||
final String watermarkTimestamp = "${data.samplingDate} ${data.samplingTime}";
|
final String watermarkTimestamp = "${data.samplingDate} ${data.samplingTime}";
|
||||||
@ -223,6 +225,10 @@ class MarineInvestigativeSamplingService {
|
|||||||
required AuthProvider authProvider,
|
required AuthProvider authProvider,
|
||||||
String? logDirectory,
|
String? logDirectory,
|
||||||
}) async {
|
}) async {
|
||||||
|
// --- START FIX: Capture the status before attempting submission ---
|
||||||
|
final String? previousStatus = data.submissionStatus;
|
||||||
|
// --- END FIX ---
|
||||||
|
|
||||||
final serverName = (await _serverConfigService.getActiveApiConfig())?['config_name'] as String? ?? 'Default';
|
final serverName = (await _serverConfigService.getActiveApiConfig())?['config_name'] as String? ?? 'Default';
|
||||||
final imageFilesWithNulls = data.toApiImageFiles();
|
final imageFilesWithNulls = data.toApiImageFiles();
|
||||||
imageFilesWithNulls.removeWhere((key, value) => value == null);
|
imageFilesWithNulls.removeWhere((key, value) => value == null);
|
||||||
@ -287,7 +293,6 @@ class MarineInvestigativeSamplingService {
|
|||||||
debugPrint("Skipping FTP attempt due to known expired session. Manually queuing FTP tasks.");
|
debugPrint("Skipping FTP attempt due to known expired session. Manually queuing FTP tasks.");
|
||||||
final baseFileNameForQueue = _generateBaseFileName(data);
|
final baseFileNameForQueue = _generateBaseFileName(data);
|
||||||
|
|
||||||
// --- START FIX: Add ftpConfigId when queuing ---
|
|
||||||
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
||||||
|
|
||||||
final dataZip = await _zippingService.createDataZip(
|
final dataZip = await _zippingService.createDataZip(
|
||||||
@ -296,14 +301,13 @@ class MarineInvestigativeSamplingService {
|
|||||||
destinationDir: null, // Use temp dir
|
destinationDir: null, // Use temp dir
|
||||||
);
|
);
|
||||||
if (dataZip != null) {
|
if (dataZip != null) {
|
||||||
// Queue for each config separately
|
|
||||||
for (final config in ftpConfigs) {
|
for (final config in ftpConfigs) {
|
||||||
final configId = config['ftp_config_id'];
|
final configId = config['ftp_config_id'];
|
||||||
if (configId != null) {
|
if (configId != null) {
|
||||||
await _retryService.addFtpToQueue(
|
await _retryService.addFtpToQueue(
|
||||||
localFilePath: dataZip.path,
|
localFilePath: dataZip.path,
|
||||||
remotePath: '/${p.basename(dataZip.path)}',
|
remotePath: '/${p.basename(dataZip.path)}',
|
||||||
ftpConfigId: configId // Provide the specific config ID
|
ftpConfigId: configId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -316,33 +320,29 @@ class MarineInvestigativeSamplingService {
|
|||||||
destinationDir: null, // Use temp dir
|
destinationDir: null, // Use temp dir
|
||||||
);
|
);
|
||||||
if (imageZip != null) {
|
if (imageZip != null) {
|
||||||
// Queue for each config separately
|
|
||||||
for (final config in ftpConfigs) {
|
for (final config in ftpConfigs) {
|
||||||
final configId = config['ftp_config_id'];
|
final configId = config['ftp_config_id'];
|
||||||
if (configId != null) {
|
if (configId != null) {
|
||||||
await _retryService.addFtpToQueue(
|
await _retryService.addFtpToQueue(
|
||||||
localFilePath: imageZip.path,
|
localFilePath: imageZip.path,
|
||||||
remotePath: '/${p.basename(imageZip.path)}',
|
remotePath: '/${p.basename(imageZip.path)}',
|
||||||
ftpConfigId: configId // Provide the specific config ID
|
ftpConfigId: configId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- END FIX ---
|
|
||||||
ftpResults = {'statuses': [{'status': 'Queued', 'message': 'FTP upload queued due to API session issue.', 'success': false}]};
|
ftpResults = {'statuses': [{'status': 'Queued', 'message': 'FTP upload queued due to API session issue.', 'success': false}]};
|
||||||
anyFtpSuccess = false;
|
anyFtpSuccess = false;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Session is OK, proceed with normal FTP attempt
|
// Session is OK, proceed with normal FTP attempt
|
||||||
try {
|
try {
|
||||||
// _generateAndUploadFtpFiles already uses the generic SubmissionFtpService
|
|
||||||
ftpResults = await _generateAndUploadFtpFiles(data, finalImageFiles, serverName, moduleName);
|
ftpResults = await _generateAndUploadFtpFiles(data, finalImageFiles, serverName, moduleName);
|
||||||
// Check if *any* configured FTP target succeeded (excluding 'Not Configured')
|
|
||||||
anyFtpSuccess = !(ftpResults['statuses'] as List).any((status) => status['success'] == false && status['status'] != 'Not Configured');
|
anyFtpSuccess = !(ftpResults['statuses'] as List).any((status) => status['success'] == false && status['status'] != 'Not Configured');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Unexpected FTP submission error: $e");
|
debugPrint("Unexpected FTP submission error: $e");
|
||||||
anyFtpSuccess = false; // FTP failures are auto-queued by SubmissionFtpService
|
anyFtpSuccess = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,7 +376,10 @@ class MarineInvestigativeSamplingService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 6. Send Alert
|
// 6. Send Alert
|
||||||
if (overallSuccess) {
|
// --- START FIX: Check if log was already successful before sending alert ---
|
||||||
|
final bool wasAlreadySuccessful = previousStatus == 'S4' || previousStatus == 'S3' || previousStatus == 'L4';
|
||||||
|
if (overallSuccess && !wasAlreadySuccessful) {
|
||||||
|
// --- END FIX ---
|
||||||
_handleInvestigativeSuccessAlert(data, appSettings, isDataOnly: finalImageFiles.isEmpty, isSessionExpired: isSessionKnownToBeExpired);
|
_handleInvestigativeSuccessAlert(data, appSettings, isDataOnly: finalImageFiles.isEmpty, isSessionExpired: isSessionKnownToBeExpired);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,9 +400,7 @@ class MarineInvestigativeSamplingService {
|
|||||||
|
|
||||||
String? savedLogPath = logDirectory; // Use existing path if provided
|
String? savedLogPath = logDirectory; // Use existing path if provided
|
||||||
|
|
||||||
// Save/Update local log first
|
|
||||||
if (savedLogPath != null && savedLogPath.isNotEmpty) {
|
if (savedLogPath != null && savedLogPath.isNotEmpty) {
|
||||||
// Prepare map with file paths for update
|
|
||||||
Map<String, dynamic> logUpdateData = data.toDbJson();
|
Map<String, dynamic> logUpdateData = data.toDbJson();
|
||||||
final imageFiles = data.toApiImageFiles();
|
final imageFiles = data.toApiImageFiles();
|
||||||
imageFiles.forEach((key, file) {
|
imageFiles.forEach((key, file) {
|
||||||
@ -429,9 +430,6 @@ class MarineInvestigativeSamplingService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
|
const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
|
||||||
// Log final queued state to central DB
|
|
||||||
// await _logAndSave(data: data, status: 'Queued', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}, logDirectory: savedLogPath);
|
|
||||||
|
|
||||||
return {'success': true, 'message': successMessage, 'reportId': null}; // No report ID yet
|
return {'success': true, 'message': successMessage, 'reportId': null}; // No report ID yet
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -444,10 +442,16 @@ class MarineInvestigativeSamplingService {
|
|||||||
} else if (data.stationTypeSelection == 'New Location') {
|
} else if (data.stationTypeSelection == 'New Location') {
|
||||||
stationCode = data.newStationCode ?? 'NEW_NA';
|
stationCode = data.newStationCode ?? 'NEW_NA';
|
||||||
}
|
}
|
||||||
final datePart = data.samplingDate ?? 'NODATE';
|
|
||||||
final timePart = (data.samplingTime ?? 'NOTIME').replaceAll(':', '-');
|
// --- START: MODIFIED (from in-situ) ---
|
||||||
final fileTimestamp = "${datePart}_${timePart}".replaceAll(' ', '_');
|
// Use reportId if available, otherwise fall back to timestamp
|
||||||
return '${stationCode}_$fileTimestamp';
|
if (data.reportId != null && data.reportId!.isNotEmpty) {
|
||||||
|
return '${stationCode}_${data.reportId}';
|
||||||
|
} else {
|
||||||
|
final fileTimestamp = "${data.samplingDate}_${data.samplingTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
||||||
|
return '${stationCode}_$fileTimestamp';
|
||||||
|
}
|
||||||
|
// --- END: MODIFIED ---
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -459,7 +463,12 @@ class MarineInvestigativeSamplingService {
|
|||||||
module: 'marine',
|
module: 'marine',
|
||||||
subModule: 'marine_investigative_sampling',
|
subModule: 'marine_investigative_sampling',
|
||||||
);
|
);
|
||||||
final folderName = data.reportId ?? baseFileName;
|
|
||||||
|
// --- START: MODIFIED folderName (from in-situ) ---
|
||||||
|
// Use baseFileName for the folder name to match [stationCode]_[reportId]
|
||||||
|
final folderName = baseFileName;
|
||||||
|
// --- END: MODIFIED folderName ---
|
||||||
|
|
||||||
final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, folderName)) : null;
|
final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, folderName)) : null;
|
||||||
|
|
||||||
if (localSubmissionDir != null && !await localSubmissionDir.exists()) {
|
if (localSubmissionDir != null && !await localSubmissionDir.exists()) {
|
||||||
@ -470,11 +479,15 @@ class MarineInvestigativeSamplingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: MODIFIED createDataZip call (from in-situ) ---
|
||||||
|
// This module does not have the extra JSON files, so we keep the single db.json
|
||||||
final dataZip = await _zippingService.createDataZip(
|
final dataZip = await _zippingService.createDataZip(
|
||||||
jsonDataMap: {'db.json': jsonEncode(data.toDbJson())},
|
jsonDataMap: {'db.json': jsonEncode(data.toDbJson())},
|
||||||
baseFileName: baseFileName,
|
baseFileName: baseFileName,
|
||||||
destinationDir: localSubmissionDir,
|
destinationDir: localSubmissionDir,
|
||||||
);
|
);
|
||||||
|
// --- END: MODIFIED createDataZip call ---
|
||||||
|
|
||||||
Map<String, dynamic> ftpDataResult = {'success': true, 'statuses': []};
|
Map<String, dynamic> ftpDataResult = {'success': true, 'statuses': []};
|
||||||
if (dataZip != null) {
|
if (dataZip != null) {
|
||||||
ftpDataResult = await _submissionFtpService.submit(
|
ftpDataResult = await _submissionFtpService.submit(
|
||||||
@ -525,13 +538,11 @@ class MarineInvestigativeSamplingService {
|
|||||||
data.submissionMessage = message;
|
data.submissionMessage = message;
|
||||||
final baseFileName = _generateBaseFileName(data);
|
final baseFileName = _generateBaseFileName(data);
|
||||||
|
|
||||||
// Prepare log data map including file paths
|
|
||||||
Map<String, dynamic> logMapData = data.toDbJson();
|
Map<String, dynamic> logMapData = data.toDbJson();
|
||||||
final imageFileMap = data.toApiImageFiles();
|
final imageFileMap = data.toApiImageFiles();
|
||||||
imageFileMap.forEach((key, file) {
|
imageFileMap.forEach((key, file) {
|
||||||
logMapData[key] = file?.path; // Store path or null
|
logMapData[key] = file?.path; // Store path or null
|
||||||
});
|
});
|
||||||
// Add submission metadata
|
|
||||||
logMapData['submissionStatus'] = status;
|
logMapData['submissionStatus'] = status;
|
||||||
logMapData['submissionMessage'] = message;
|
logMapData['submissionMessage'] = message;
|
||||||
logMapData['reportId'] = data.reportId;
|
logMapData['reportId'] = data.reportId;
|
||||||
@ -568,10 +579,11 @@ class MarineInvestigativeSamplingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: MODIFIED ALERT HANDLER ---
|
||||||
Future<void> _handleInvestigativeSuccessAlert(MarineInvesManualSamplingData data, List<Map<String, dynamic>>? appSettings, {required bool isDataOnly, bool isSessionExpired = false}) async {
|
Future<void> _handleInvestigativeSuccessAlert(MarineInvesManualSamplingData data, List<Map<String, dynamic>>? appSettings, {required bool isDataOnly, bool isSessionExpired = false}) async {
|
||||||
|
|
||||||
String generateInvestigativeTelegramAlertMessage(MarineInvesManualSamplingData data, {required bool isDataOnly}) {
|
// This internal function generates the main message
|
||||||
|
Future<String> generateInvestigativeTelegramAlertMessage(MarineInvesManualSamplingData data, {required bool isDataOnly}) async {
|
||||||
final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)";
|
final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)";
|
||||||
|
|
||||||
String stationName = 'N/A';
|
String stationName = 'N/A';
|
||||||
@ -588,31 +600,54 @@ class MarineInvestigativeSamplingService {
|
|||||||
stationCode = data.newStationCode ?? 'NEW';
|
stationCode = data.newStationCode ?? 'NEW';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final submissionDate = data.samplingDate ?? DateFormat('yyyy-MM-dd').format(DateTime.now());
|
||||||
|
final submissionTime = data.samplingTime ?? DateFormat('HH:mm:ss').format(DateTime.now());
|
||||||
|
final submitter = data.firstSamplerName ?? 'N/A';
|
||||||
|
|
||||||
final buffer = StringBuffer()
|
final buffer = StringBuffer()
|
||||||
..writeln('🕵️ *Marine Investigative Sample $submissionType Submitted:*')
|
..writeln('🕵️ *Marine Investigative Sample $submissionType Submitted:*')
|
||||||
..writeln()
|
..writeln()
|
||||||
..writeln('*Station Name & Code:* $stationName ($stationCode)')
|
..writeln('*Station Name & Code:* $stationName ($stationCode)')
|
||||||
..writeln('*Date of Submitted:* ${data.samplingDate}')
|
..writeln('*Date & Time of Submission:* $submissionDate $submissionTime')
|
||||||
..writeln('*Submitted by User:* ${data.firstSamplerName}')
|
..writeln('*Submitted by User:* $submitter')
|
||||||
..writeln('*Sonde ID:* ${data.sondeId ?? "N/A"}')
|
..writeln('*Sonde ID:* ${data.sondeId ?? "N/A"}')
|
||||||
..writeln('*Status of Submission:* Successful');
|
..writeln('*Status of Submission:* Successful');
|
||||||
|
|
||||||
if (data.distanceDifferenceInKm != null && data.distanceDifferenceInKm! * 1000 > 50) {
|
final distanceKm = data.distanceDifferenceInKm ?? 0;
|
||||||
|
final distanceMeters = (distanceKm * 1000).toStringAsFixed(0);
|
||||||
|
final distanceRemarks = data.distanceDifferenceRemarks ?? 'N/A';
|
||||||
|
if (distanceKm * 1000 > 50 || (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A')) {
|
||||||
buffer
|
buffer
|
||||||
..writeln()
|
..writeln()
|
||||||
..writeln('🔔 *Distance Alert:*')
|
..writeln('🔔 *Distance Alert:*')
|
||||||
..writeln('*Distance from station:* ${(data.distanceDifferenceInKm! * 1000).toStringAsFixed(0)} meters');
|
..writeln('*Distance from station:* $distanceMeters meters (${distanceKm.toStringAsFixed(3)} KM)');
|
||||||
|
|
||||||
if (data.distanceDifferenceRemarks != null && data.distanceDifferenceRemarks!.isNotEmpty) {
|
if (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A') {
|
||||||
buffer.writeln('*Remarks for distance:* ${data.distanceDifferenceRemarks}');
|
buffer.writeln('*Remarks for distance:* $distanceRemarks');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- NEW: Add parameter limit checks to message ---
|
||||||
|
// 1. Add station parameter limit check section
|
||||||
|
final outOfBoundsAlert = await _getOutOfBoundsAlertSection(data);
|
||||||
|
if (outOfBoundsAlert.isNotEmpty) {
|
||||||
|
buffer.write(outOfBoundsAlert);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Add NPE parameter limit check section
|
||||||
|
final npeAlert = await _getNpeAlertSection(data);
|
||||||
|
if (npeAlert.isNotEmpty) {
|
||||||
|
buffer.write(npeAlert);
|
||||||
|
}
|
||||||
|
// --- END NEW ---
|
||||||
|
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
// --- End internal function ---
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final message = generateInvestigativeTelegramAlertMessage(data, isDataOnly: isDataOnly);
|
// Call the internal function to build the message
|
||||||
|
final message = await generateInvestigativeTelegramAlertMessage(data, isDataOnly: isDataOnly);
|
||||||
final alertKey = 'marine_investigative';
|
final alertKey = 'marine_investigative';
|
||||||
|
|
||||||
if (isSessionExpired) {
|
if (isSessionExpired) {
|
||||||
@ -628,4 +663,161 @@ class MarineInvestigativeSamplingService {
|
|||||||
debugPrint("Failed to handle Investigative Telegram alert: $e");
|
debugPrint("Failed to handle Investigative Telegram alert: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- NEW: Added from in-situ service ---
|
||||||
|
/// Helper to generate the station-specific parameter limit alert section.
|
||||||
|
Future<String> _getOutOfBoundsAlertSection(MarineInvesManualSamplingData data) async {
|
||||||
|
// Only check limits if it's a Manual Station
|
||||||
|
if (data.stationTypeSelection != 'Existing Manual Station') {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Map<String, String> _parameterKeyToLimitName = {
|
||||||
|
'oxygenConcentration': 'Oxygen Conc', 'oxygenSaturation': 'Oxygen Sat', 'ph': 'pH',
|
||||||
|
'salinity': 'Salinity', 'electricalConductivity': 'Conductivity', 'temperature': 'Temperature',
|
||||||
|
'tds': 'TDS', 'turbidity': 'Turbidity', 'tss': 'TSS', 'batteryVoltage': 'Battery',
|
||||||
|
};
|
||||||
|
|
||||||
|
final allLimits = await _dbHelper.loadMarineParameterLimits() ?? [];
|
||||||
|
if (allLimits.isEmpty) return "";
|
||||||
|
|
||||||
|
final dynamic stationId = data.selectedStation?['man_station_id'];
|
||||||
|
if (stationId == null) return ""; // Cannot check limits
|
||||||
|
|
||||||
|
final readings = {
|
||||||
|
'oxygenConcentration': data.oxygenConcentration, 'oxygenSaturation': data.oxygenSaturation,
|
||||||
|
'ph': data.ph, 'salinity': data.salinity, 'electricalConductivity': data.electricalConductivity,
|
||||||
|
'temperature': data.temperature, 'tds': data.tds, 'turbidity': data.turbidity,
|
||||||
|
'tss': data.tss, 'batteryVoltage': data.batteryVoltage,
|
||||||
|
};
|
||||||
|
|
||||||
|
final List<String> outOfBoundsMessages = [];
|
||||||
|
|
||||||
|
double? parseLimitValue(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is num) return value.toDouble();
|
||||||
|
if (value is String) return double.tryParse(value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
readings.forEach((key, value) {
|
||||||
|
if (value == null || value == -999.0) return;
|
||||||
|
|
||||||
|
final limitName = _parameterKeyToLimitName[key];
|
||||||
|
if (limitName == null) return;
|
||||||
|
|
||||||
|
final limitData = allLimits.firstWhere(
|
||||||
|
(l) => l['param_parameter_list'] == limitName && l['station_id']?.toString() == stationId.toString(),
|
||||||
|
orElse: () => <String, dynamic>{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (limitData.isNotEmpty) {
|
||||||
|
final lowerLimit = parseLimitValue(limitData['param_lower_limit']);
|
||||||
|
final upperLimit = parseLimitValue(limitData['param_upper_limit']);
|
||||||
|
|
||||||
|
if ((lowerLimit != null && value < lowerLimit) || (upperLimit != null && value > upperLimit)) {
|
||||||
|
final valueStr = value.toStringAsFixed(5);
|
||||||
|
final lowerStr = lowerLimit?.toStringAsFixed(5) ?? 'N/A';
|
||||||
|
final upperStr = upperLimit?.toStringAsFixed(5) ?? 'N/A';
|
||||||
|
outOfBoundsMessages.add('- *$limitName*: `$valueStr` (Station Limit: `$lowerStr` - `$upperStr`)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (outOfBoundsMessages.isEmpty) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
final buffer = StringBuffer()
|
||||||
|
..writeln()
|
||||||
|
..writeln('⚠️ *Station Parameter Limit Alert:*')
|
||||||
|
..writeln('The following parameters were outside their defined station limits:');
|
||||||
|
buffer.writeAll(outOfBoundsMessages, '\n');
|
||||||
|
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- NEW: Added from in-situ service ---
|
||||||
|
/// Helper to generate the NPE parameter limit alert section.
|
||||||
|
Future<String> _getNpeAlertSection(MarineInvesManualSamplingData data) async {
|
||||||
|
const Map<String, String> _parameterKeyToLimitName = {
|
||||||
|
'oxygenConcentration': 'Oxygen Conc', 'oxygenSaturation': 'Oxygen Sat', 'ph': 'pH',
|
||||||
|
'salinity': 'Salinity', 'electricalConductivity': 'Conductivity', 'temperature': 'Temperature',
|
||||||
|
'tds': 'TDS', 'turbidity': 'Turbidity', 'tss': 'TSS',
|
||||||
|
};
|
||||||
|
|
||||||
|
final npeLimits = await _dbHelper.loadNpeParameterLimits() ?? [];
|
||||||
|
if (npeLimits.isEmpty) return "";
|
||||||
|
|
||||||
|
final readings = {
|
||||||
|
'oxygenConcentration': data.oxygenConcentration, 'oxygenSaturation': data.oxygenSaturation,
|
||||||
|
'ph': data.ph, 'salinity': data.salinity, 'electricalConductivity': data.electricalConductivity,
|
||||||
|
'temperature': data.temperature, 'tds': data.tds, 'turbidity': data.turbidity,
|
||||||
|
'tss': data.tss,
|
||||||
|
};
|
||||||
|
|
||||||
|
final List<String> npeMessages = [];
|
||||||
|
|
||||||
|
double? parseLimitValue(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is num) return value.toDouble();
|
||||||
|
if (value is String) return double.tryParse(value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
readings.forEach((key, value) {
|
||||||
|
if (value == null || value == -999.0) return;
|
||||||
|
|
||||||
|
final limitName = _parameterKeyToLimitName[key];
|
||||||
|
if (limitName == null) return;
|
||||||
|
|
||||||
|
final limitData = npeLimits.firstWhere(
|
||||||
|
(l) => l['param_parameter_list'] == limitName,
|
||||||
|
orElse: () => <String, dynamic>{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (limitData.isNotEmpty) {
|
||||||
|
final lowerLimit = parseLimitValue(limitData['param_lower_limit']);
|
||||||
|
final upperLimit = parseLimitValue(limitData['param_upper_limit']);
|
||||||
|
bool isHit = false;
|
||||||
|
|
||||||
|
if (lowerLimit != null && upperLimit != null) {
|
||||||
|
if (value >= lowerLimit && value <= upperLimit) isHit = true;
|
||||||
|
} else if (lowerLimit != null && upperLimit == null) {
|
||||||
|
if (value >= lowerLimit) isHit = true;
|
||||||
|
} else if (upperLimit != null && lowerLimit == null) {
|
||||||
|
if (value <= upperLimit) isHit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHit) {
|
||||||
|
final valueStr = value.toStringAsFixed(5);
|
||||||
|
final lowerStr = lowerLimit?.toStringAsFixed(5) ?? 'N/A';
|
||||||
|
final upperStr = upperLimit?.toStringAsFixed(5) ?? 'N/á';
|
||||||
|
String limitStr;
|
||||||
|
if (lowerStr != 'N/A' && upperStr != 'N/A') {
|
||||||
|
limitStr = '$lowerStr - $upperStr';
|
||||||
|
} else if (lowerStr != 'N/A') {
|
||||||
|
limitStr = '>= $lowerStr';
|
||||||
|
} else {
|
||||||
|
limitStr = '<= $upperStr';
|
||||||
|
}
|
||||||
|
npeMessages.add('- *$limitName*: `$valueStr` (NPE Limit: `$limitStr`)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (npeMessages.isEmpty) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
final buffer = StringBuffer()
|
||||||
|
..writeln()
|
||||||
|
..writeln(' ')
|
||||||
|
..writeln('🚨 *NPE Parameter Limit Detected:*')
|
||||||
|
..writeln('The following parameters triggered an NPE alert:');
|
||||||
|
buffer.writeAll(npeMessages, '\n');
|
||||||
|
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
// --- END: MODIFIED ALERT HANDLER & HELPERS ---
|
||||||
}
|
}
|
||||||
@ -437,12 +437,21 @@ class RiverInSituSamplingService {
|
|||||||
return {'status': 'Queued', 'success': true, 'message': successMessage, 'reportId': null};
|
return {'status': 'Queued', 'success': true, 'message': successMessage, 'reportId': null};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: MODIFIED _generateBaseFileName ---
|
||||||
/// Helper to generate the base filename for ZIP files.
|
/// Helper to generate the base filename for ZIP files.
|
||||||
String _generateBaseFileName(RiverInSituSamplingData data) {
|
String _generateBaseFileName(RiverInSituSamplingData data) {
|
||||||
final stationCode = data.selectedStation?['sampling_station_code'] ?? 'UNKNOWN';
|
final stationCode = data.selectedStation?['sampling_station_code'] ?? 'UNKNOWN';
|
||||||
final fileTimestamp = "${data.samplingDate}_${data.samplingTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
|
||||||
return "${stationCode}_$fileTimestamp";
|
// Check if reportId (timestamp) is available.
|
||||||
|
if (data.reportId != null && data.reportId!.isNotEmpty) {
|
||||||
|
return '${stationCode}_${data.reportId}';
|
||||||
|
} else {
|
||||||
|
// Fallback to old method if reportId is not available (e.g., offline queue)
|
||||||
|
final fileTimestamp = "${data.samplingDate}_${data.samplingTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
||||||
|
return "${stationCode}_$fileTimestamp";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFIED _generateBaseFileName ---
|
||||||
|
|
||||||
/// Generates data and image ZIP files and uploads them using SubmissionFtpService.
|
/// Generates data and image ZIP files and uploads them using SubmissionFtpService.
|
||||||
Future<Map<String, dynamic>> _generateAndUploadFtpFiles(RiverInSituSamplingData data, Map<String, File> imageFiles, String serverName, String moduleName) async {
|
Future<Map<String, dynamic>> _generateAndUploadFtpFiles(RiverInSituSamplingData data, Map<String, File> imageFiles, String serverName, String moduleName) async {
|
||||||
@ -450,7 +459,11 @@ class RiverInSituSamplingService {
|
|||||||
|
|
||||||
final Directory? logDirectory = await _localStorageService.getRiverInSituBaseDir(data.samplingType, serverName: serverName); // Use correct base dir getter
|
final Directory? logDirectory = await _localStorageService.getRiverInSituBaseDir(data.samplingType, serverName: serverName); // Use correct base dir getter
|
||||||
|
|
||||||
final folderName = data.reportId ?? baseFileName;
|
// --- START: MODIFIED folderName ---
|
||||||
|
// Use baseFileName for the folder name to match [stationCode]_[reportId]
|
||||||
|
final folderName = baseFileName;
|
||||||
|
// --- END: MODIFIED folderName ---
|
||||||
|
|
||||||
final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, folderName)) : null;
|
final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, folderName)) : null;
|
||||||
if (localSubmissionDir != null && !await localSubmissionDir.exists()) {
|
if (localSubmissionDir != null && !await localSubmissionDir.exists()) {
|
||||||
await localSubmissionDir.create(recursive: true);
|
await localSubmissionDir.create(recursive: true);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user