repair marine investigative module
This commit is contained in:
parent
fa5f0361de
commit
882386c522
@ -32,6 +32,7 @@ class InSituSamplingData {
|
||||
String? weather;
|
||||
String? tideLevel;
|
||||
String? seaCondition;
|
||||
// String? tarball; // <-- REMOVED THIS PROPERTY
|
||||
String? eventRemarks;
|
||||
String? labRemarks;
|
||||
|
||||
@ -201,6 +202,7 @@ class InSituSamplingData {
|
||||
data.weather = json['weather'];
|
||||
data.tideLevel = json['tide_level'];
|
||||
data.seaCondition = json['sea_condition'];
|
||||
// data.tarball = json['tarball']; // <-- REMOVED DESERIALIZATION
|
||||
data.eventRemarks = json['event_remarks'];
|
||||
data.labRemarks = json['lab_remarks'];
|
||||
data.optionalRemark1 = json['man_optional_photo_01_remarks'];
|
||||
@ -254,8 +256,8 @@ class InSituSamplingData {
|
||||
return data;
|
||||
}
|
||||
|
||||
/// Creates a single JSON object with all submission data for offline storage.
|
||||
Map<String, dynamic> toDbJson() {
|
||||
/// Creates a Map object with all submission data for local logging.
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'first_sampler_name': firstSamplerName,
|
||||
'first_sampler_user_id': firstSamplerUserId,
|
||||
@ -276,6 +278,7 @@ class InSituSamplingData {
|
||||
'weather': weather,
|
||||
'tide_level': tideLevel,
|
||||
'sea_condition': seaCondition,
|
||||
// 'tarball': tarball, // <-- REMOVED
|
||||
'event_remarks': eventRemarks,
|
||||
'lab_remarks': labRemarks,
|
||||
'man_optional_photo_01_remarks': optionalRemark1,
|
||||
@ -305,8 +308,101 @@ class InSituSamplingData {
|
||||
};
|
||||
}
|
||||
|
||||
// --- REMOVED: generateTelegramAlertMessage method ---
|
||||
// This logic is now in MarineInSituSamplingService
|
||||
/// Creates a single JSON object with all submission data, mimicking 'db.json'
|
||||
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() {
|
||||
final Map<String, String> map = {};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data'; // <-- ADDED: Required for Uint8List
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -26,7 +27,7 @@ class MarineInvesManualStep2SiteInfo extends StatefulWidget {
|
||||
|
||||
class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2SiteInfo> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isPickingImage = false;
|
||||
bool _isPickingImage = false; // <-- ADDED: State variable from in-situ
|
||||
|
||||
late final TextEditingController _eventRemarksController;
|
||||
late final TextEditingController _labRemarksController;
|
||||
@ -49,6 +50,7 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- START: MODIFIED _setImage function (from in-situ) ---
|
||||
/// Handles picking and processing an image using the dedicated service.
|
||||
void _setImage(Function(File?) setImageCallback, ImageSource source, String imageInfo, {required bool isRequired}) async {
|
||||
if (_isPickingImage) return;
|
||||
@ -56,19 +58,23 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
||||
|
||||
final service = Provider.of<MarineInvestigativeSamplingService>(context, listen: false);
|
||||
|
||||
// The service's pickAndProcessImage method will handle file naming
|
||||
final file = await service.pickAndProcessImage(source, data: widget.data, imageInfo: imageInfo, isRequired: isRequired);
|
||||
// Always pass `isRequired: true` to the service to enforce landscape check
|
||||
// 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) {
|
||||
setState(() => setImageCallback(file));
|
||||
} 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) {
|
||||
setState(() => _isPickingImage = false);
|
||||
}
|
||||
}
|
||||
// --- END: MODIFIED _setImage function ---
|
||||
|
||||
/// Validates the form and all required images before proceeding.
|
||||
void _goToNextStep() {
|
||||
@ -142,7 +148,11 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
||||
|
||||
// --- Section: Required Photos ---
|
||||
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),
|
||||
_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),
|
||||
@ -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}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
@ -207,14 +218,63 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
||||
const SizedBox(height: 8),
|
||||
if (imageFile != null)
|
||||
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
|
||||
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();
|
||||
}
|
||||
|
||||
// --- START: MODIFIED LIFECYCLE METHOD ---
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (mounted) {
|
||||
// Use the member variable, not context
|
||||
final service = _samplingService;
|
||||
final btConnecting = service.bluetoothConnectionState.value == BluetoothConnectionState.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) {
|
||||
// Force-call disconnect to reset both the service's state
|
||||
// and the local _isLoading flag (inside _disconnect).
|
||||
_disconnectFromAll();
|
||||
} else {
|
||||
// If not stuck, just a normal refresh
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- END: MODIFIED LIFECYCLE METHOD ---
|
||||
|
||||
void _initializeControllers() {
|
||||
widget.data.dataCaptureDate = widget.data.samplingDate;
|
||||
@ -299,9 +292,7 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
||||
});
|
||||
}
|
||||
|
||||
// --- START: MODIFIED DISCONNECT METHOD ---
|
||||
void _disconnect(String type) {
|
||||
// Use the member variable, not context, for lifecycle safety
|
||||
final service = _samplingService;
|
||||
if (type == 'bluetooth') {
|
||||
service.disconnectFromBluetooth();
|
||||
@ -315,15 +306,12 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
||||
setState(() {
|
||||
_isAutoReading = false;
|
||||
_isLockedOut = false;
|
||||
_isLoading = false; // <-- CRITICAL: Also reset the loading flag
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
// --- END: MODIFIED DISCONNECT METHOD ---
|
||||
|
||||
// --- START: MODIFIED DISCONNECT_ALL METHOD ---
|
||||
void _disconnectFromAll() {
|
||||
// Use the member variable, not context, for lifecycle safety
|
||||
final service = _samplingService;
|
||||
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||
_disconnect('bluetooth');
|
||||
@ -332,7 +320,6 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
||||
_disconnect('serial');
|
||||
}
|
||||
}
|
||||
// --- END: MODIFIED DISCONNECT_ALL METHOD ---
|
||||
|
||||
void _updateTextFields(Map<String, double> readings) {
|
||||
const defaultValue = -999.0;
|
||||
@ -350,6 +337,7 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
||||
});
|
||||
}
|
||||
|
||||
// --- START: MODIFIED _validateAndProceed ---
|
||||
void _validateAndProceed() {
|
||||
if (_isLockedOut) {
|
||||
_showSnackBar("Please wait for the initial reading period to complete.", isError: true);
|
||||
@ -366,13 +354,25 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
||||
_formKey.currentState!.save();
|
||||
|
||||
final currentReadings = _captureReadingsToMap();
|
||||
List<Map<String, dynamic>> outOfBoundsParams = [];
|
||||
|
||||
// --- NEW CONDITIONAL LOGIC ---
|
||||
// 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 ?? [];
|
||||
final outOfBoundsParams = _validateParameters(currentReadings, marineLimits);
|
||||
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) {
|
||||
_showParameterLimitDialog(outOfBoundsParams, currentReadings);
|
||||
@ -380,6 +380,7 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
||||
_saveDataAndMoveOn(currentReadings);
|
||||
}
|
||||
}
|
||||
// --- END: MODIFIED _validateAndProceed ---
|
||||
|
||||
Map<String, double> _captureReadingsToMap() {
|
||||
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) {
|
||||
final List<Map<String, dynamic>> invalidParams = [];
|
||||
|
||||
// --- MODIFIED: Get station ID based on station type ---
|
||||
int? stationId;
|
||||
// This check is now redundant due to _validateAndProceed, but safe to keep
|
||||
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("Selected Station ID: $stationId");
|
||||
debugPrint("Selected Station ID: $stationId (from 'man_station_id')");
|
||||
|
||||
double? _parseLimitValue(dynamic value) {
|
||||
if (value == null) return null;
|
||||
@ -824,8 +823,11 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
||||
if (isConnecting || _isLoading)
|
||||
const CircularProgressIndicator()
|
||||
else if (isConnected)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
Wrap(
|
||||
alignment: WrapAlignment.spaceEvenly,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 8.0,
|
||||
runSpacing: 4.0,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
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
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data'; // <-- Required for Uint8List
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../../auth_provider.dart';
|
||||
import '../../../../models/marine_inves_manual_sampling_data.dart';
|
||||
// REMOVED: Import for NPE Report Screen is no longer needed
|
||||
// import '../reports/npe_report_from_investigative.dart';
|
||||
|
||||
class MarineInvesManualStep4Summary extends StatefulWidget {
|
||||
final MarineInvesManualSamplingData data;
|
||||
@ -29,7 +29,6 @@ class MarineInvesManualStep4Summary extends StatefulWidget {
|
||||
class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Summary> {
|
||||
bool _isHandlingSubmit = false;
|
||||
|
||||
// Keep parameter names for highlighting out-of-bounds station limits
|
||||
static const Map<String, String> _parameterKeyToLimitName = {
|
||||
'oxygenConcentration': 'Oxygen Conc',
|
||||
'oxygenSaturation': 'Oxygen Sat',
|
||||
@ -43,18 +42,15 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
||||
'batteryVoltage': 'Battery',
|
||||
};
|
||||
|
||||
// Keep this function to highlight parameters outside *station* limits
|
||||
Set<String> _getOutOfBoundsKeys(BuildContext context) {
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
// Use regular marine limits, not NPE limits
|
||||
final marineLimits = authProvider.marineParameterLimits ?? [];
|
||||
final Set<String> invalidKeys = {};
|
||||
|
||||
int? stationId;
|
||||
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 = {
|
||||
'oxygenConcentration': widget.data.oxygenConcentration,
|
||||
@ -88,7 +84,7 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
||||
limitData = marineLimits.firstWhere(
|
||||
(l) =>
|
||||
l['param_parameter_list'] == limitName &&
|
||||
l['station_id'] == stationId,
|
||||
l['station_id']?.toString() == stationId.toString(),
|
||||
orElse: () => {},
|
||||
);
|
||||
}
|
||||
@ -107,17 +103,226 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
||||
return invalidKeys;
|
||||
}
|
||||
|
||||
// REMOVED: _getNpeTriggeredParameters method
|
||||
// REMOVED: _showNpeDialog method
|
||||
// --- START: ADDED from in-situ module ---
|
||||
/// 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 {
|
||||
if (_isHandlingSubmit || widget.isLoading) return;
|
||||
|
||||
setState(() => _isHandlingSubmit = true);
|
||||
|
||||
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();
|
||||
if (!mounted) return;
|
||||
|
||||
@ -133,6 +338,7 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
||||
|
||||
// If submission was successful, navigate back to the home screen
|
||||
if (result['success'] == true) {
|
||||
// No conditional navigation, just go home
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
// 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
|
||||
List<Widget> _buildStationDetails() {
|
||||
@ -190,7 +397,6 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Still get out-of-bounds keys for station limits to highlight them
|
||||
final outOfBoundsKeys = _getOutOfBoundsKeys(context);
|
||||
|
||||
return ListView(
|
||||
@ -214,7 +420,7 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
||||
_buildDetailRow("Sampling Type:", widget.data.samplingType),
|
||||
_buildDetailRow("Sample ID Code:", widget.data.sampleIdCode),
|
||||
const Divider(height: 20),
|
||||
..._buildStationDetails(), // Use dynamic station details
|
||||
..._buildStationDetails(),
|
||||
],
|
||||
),
|
||||
_buildSectionCard(
|
||||
@ -346,7 +552,7 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
||||
(widget.isLoading || _isHandlingSubmit)
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ElevatedButton.icon(
|
||||
onPressed: () => _handleSubmit(context), // Simplified call
|
||||
onPressed: () => _handleSubmit(context),
|
||||
icon: const Icon(Icons.cloud_upload),
|
||||
label: const Text('Confirm & Submit'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
@ -420,7 +626,7 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
||||
isMissing ? 'N/A' : '${value.toStringAsFixed(5)} ${unit}'.trim();
|
||||
final Color? defaultTextColor =
|
||||
Theme.of(context).textTheme.bodyLarge?.color;
|
||||
final Color valueColor = isOutOfBounds // Still highlight if outside station limits
|
||||
final Color valueColor = isOutOfBounds
|
||||
? Colors.red
|
||||
: (isMissing ? Colors.grey : defaultTextColor ?? Colors.black);
|
||||
|
||||
@ -453,11 +659,34 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
||||
if (image != null)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
child: Image.file(image,
|
||||
key: UniqueKey(),
|
||||
child: FutureBuilder<Uint8List>(
|
||||
key: ValueKey(image.path),
|
||||
future: image.readAsBytes(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover),
|
||||
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
|
||||
Container(
|
||||
|
||||
@ -94,6 +94,52 @@ class _MarineManualReportStatusLogState
|
||||
bool _isLoading = true;
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -152,9 +198,7 @@ class _MarineManualReportStatusLogState
|
||||
tempPreSampling.add(SubmissionLogEntry(
|
||||
type: 'Pre-Departure Checklist',
|
||||
title: 'Pre-Departure Checklist',
|
||||
// --- START: REVERTED ---
|
||||
stationCode: 'N/A', // Reverted
|
||||
// --- END: REVERTED ---
|
||||
stationCode: 'N/A',
|
||||
// --- START: MODIFIED ---
|
||||
senderName: (log['reporterName'] as String?) ?? 'Unknown User',
|
||||
// --- END: MODIFIED ---
|
||||
@ -317,7 +361,6 @@ class _MarineManualReportStatusLogState
|
||||
final data = MarineManualPreDepartureChecklistData();
|
||||
data.reporterUserId = logData['reporterUserId'];
|
||||
data.submissionDate = logData['submissionDate'];
|
||||
// data.location = logData['location']; // <-- REVERTED
|
||||
// Reconstruct maps
|
||||
if (logData['checklistItems'] != null) {
|
||||
data.checklistItems =
|
||||
@ -526,7 +569,7 @@ class _MarineManualReportStatusLogState
|
||||
_preSamplingSearchController,
|
||||
),
|
||||
_buildCategorySection(
|
||||
'Report Log',
|
||||
'NPE Report Log',
|
||||
_filteredReportLogs,
|
||||
_reportSearchController,
|
||||
),
|
||||
@ -732,6 +775,7 @@ class _MarineManualReportStatusLogState
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
// --- START: ADDED WIDGET-BASED HEADER HELPER ---
|
||||
/// Builds a formatted category header row for the data list.
|
||||
Widget _buildCategoryHeader(BuildContext context, String title, IconData icon) {
|
||||
return Padding(
|
||||
@ -752,22 +796,50 @@ class _MarineManualReportStatusLogState
|
||||
),
|
||||
);
|
||||
}
|
||||
// --- END: ADDED WIDGET-BASED HEADER HELPER ---
|
||||
|
||||
/// Builds a formatted row for the data list.
|
||||
Widget _buildDataRow(String label, String? value) {
|
||||
// --- START: RE-INTRODUCED TABLE-BASED HELPERS ---
|
||||
/// 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;
|
||||
|
||||
if (displayValue == '-999.0' || displayValue == '-999') {
|
||||
displayValue = 'N/A';
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
return TableRow(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
@ -776,36 +848,40 @@ class _MarineManualReportStatusLogState
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||
child: Text(
|
||||
displayValue,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
style: TextStyle(fontSize: 14.0, color: valueColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- START: NEW HELPER FOR PRE-DEPARTURE ---
|
||||
/// Determines the category for a given checklist item.
|
||||
String _getChecklistCategory(String itemName) {
|
||||
final lowerItem = itemName.toLowerCase();
|
||||
if (lowerItem.contains('in-situ') || lowerItem.contains('sonde') || lowerItem.contains('van dorn')) {
|
||||
return 'Internal - In-Situ Sampling';
|
||||
/// Builds a remark row for the data dialog.
|
||||
TableRow _buildRemarkTableRow(String? remark) {
|
||||
if (remark == null || remark.isEmpty) {
|
||||
return const TableRow(children: [SizedBox.shrink(), SizedBox.shrink()]);
|
||||
}
|
||||
if (lowerItem.contains('tarball')) {
|
||||
return 'Internal - Tarball Sampling';
|
||||
return TableRow(
|
||||
children: [
|
||||
const SizedBox.shrink(), // Empty cell for the label
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0, left: 8.0, right: 8.0),
|
||||
child: Text(
|
||||
"Remark: $remark",
|
||||
style: TextStyle(
|
||||
fontSize: 13.0,
|
||||
color: Colors.grey.shade700,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (lowerItem.contains('laboratory') || lowerItem.contains('ice') || lowerItem.contains('bottle')) {
|
||||
return 'External - Laboratory';
|
||||
}
|
||||
// Add more rules here if needed
|
||||
return 'General';
|
||||
}
|
||||
// --- END: NEW HELPER FOR PRE-DEPARTURE ---
|
||||
// --- END: RE-INTRODUCED TABLE-BASED HELPERS ---
|
||||
|
||||
/// Shows the categorized and formatted data log in a dialog
|
||||
void _showDataDialog(BuildContext context, SubmissionLogEntry log) {
|
||||
@ -813,45 +889,31 @@ class _MarineManualReportStatusLogState
|
||||
Widget dialogContent; // This will hold either a ListView or a Column
|
||||
|
||||
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 remarks = Map<String, String>.from(data['remarks'] ?? {});
|
||||
|
||||
// 1. Group items by category
|
||||
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
|
||||
// 1. Build the list of widgets
|
||||
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
|
||||
for (final categoryEntry in _checklistSections.entries) {
|
||||
final categoryTitle = categoryEntry.key;
|
||||
final categoryItems = categoryEntry.value;
|
||||
|
||||
// Add the category header
|
||||
contentWidgets.add(_buildCategoryHeader(context, categoryTitle, Icons.check_box_outlined));
|
||||
|
||||
// 3. Add the items for that category
|
||||
contentWidgets.addAll(
|
||||
categorizedItems[category]!.map((entry) {
|
||||
final key = entry.key;
|
||||
final value = entry.value;
|
||||
final status = value ? 'Yes' : 'No';
|
||||
final remark = remarks[key] ?? '';
|
||||
categoryItems.map((itemName) {
|
||||
// Find the item's status and remark from the log data
|
||||
final bool value = items[itemName] ?? false; // Default to 'No'
|
||||
final String remark = remarks[itemName] ?? '';
|
||||
final String status = value ? 'Yes' : 'No';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -863,7 +925,7 @@ class _MarineManualReportStatusLogState
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
key,
|
||||
itemName, // Use the name from the category list
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14.0,
|
||||
@ -923,6 +985,54 @@ class _MarineManualReportStatusLogState
|
||||
// Add a divider after the category
|
||||
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) {
|
||||
@ -937,25 +1047,20 @@ class _MarineManualReportStatusLogState
|
||||
// --- END: Handle Pre-Departure Checklist ---
|
||||
|
||||
} else {
|
||||
// --- START: Handle ALL OTHER Log Types (uses Column) ---
|
||||
final List<Widget> contentWidgets = [];
|
||||
// --- START: Handle ALL OTHER Log Types (uses Table) ---
|
||||
final List<TableRow> tableRows = [];
|
||||
|
||||
// --- Helper for nested maps ---
|
||||
void addNestedMapRows(Map<String, dynamic> map) {
|
||||
map.forEach((key, value) {
|
||||
if (value is Map) {
|
||||
// Handle nested maps (e.g., ysiSensorChecks)
|
||||
contentWidgets.add(_buildDataRow(key, ''));
|
||||
tableRows.add(_buildDataTableRow(key, ''));
|
||||
value.forEach((subKey, subValue) {
|
||||
contentWidgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: _buildDataRow(subKey, subValue?.toString() ?? 'N/A'),
|
||||
)
|
||||
);
|
||||
tableRows.add(_buildDataTableRow(' $subKey', subValue?.toString() ?? 'N/A'));
|
||||
});
|
||||
} 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) {
|
||||
case 'Sonde Calibration':
|
||||
contentWidgets.add(_buildCategoryHeader(context, 'Sonde Info', Icons.info_outline));
|
||||
contentWidgets.add(_buildDataRow('Sonde Serial #', _getString(data, 'sondeSerialNumber')));
|
||||
contentWidgets.add(_buildDataRow('Firmware Version', _getString(data, 'firmwareVersion')));
|
||||
contentWidgets.add(_buildDataRow('KOR Version', _getString(data, 'korVersion')));
|
||||
contentWidgets.add(_buildDataRow('Location', _getString(data, 'location')));
|
||||
contentWidgets.add(_buildDataRow('Start Time', _getString(data, 'startDateTime')));
|
||||
contentWidgets.add(_buildDataRow('End Time', _getString(data, 'endDateTime')));
|
||||
contentWidgets.add(_buildDataRow('Status', _getString(data, 'calibration_status')));
|
||||
contentWidgets.add(_buildDataRow('Remarks', _getString(data, 'remarks')));
|
||||
tableRows.add(_buildCategoryRow(context, 'Sonde Info', Icons.info_outline));
|
||||
tableRows.add(_buildDataTableRow('Sonde Serial #', _getString(data, 'sondeSerialNumber')));
|
||||
tableRows.add(_buildDataTableRow('Firmware Version', _getString(data, 'firmwareVersion')));
|
||||
tableRows.add(_buildDataTableRow('KOR Version', _getString(data, 'korVersion')));
|
||||
tableRows.add(_buildDataTableRow('Location', _getString(data, 'location')));
|
||||
tableRows.add(_buildDataTableRow('Start Time', _getString(data, 'startDateTime')));
|
||||
tableRows.add(_buildDataTableRow('End Time', _getString(data, 'endDateTime')));
|
||||
tableRows.add(_buildDataTableRow('Status', _getString(data, 'calibration_status')));
|
||||
tableRows.add(_buildDataTableRow('Remarks', _getString(data, 'remarks')));
|
||||
|
||||
contentWidgets.add(_buildCategoryHeader(context, 'pH 7.0', Icons.science_outlined));
|
||||
contentWidgets.add(_buildDataRow('MV', _getString(data, 'ph_7_mv')));
|
||||
contentWidgets.add(_buildDataRow('Before', _getString(data, 'ph_7_before')));
|
||||
contentWidgets.add(_buildDataRow('After', _getString(data, 'ph_7_after')));
|
||||
tableRows.add(_buildCategoryRow(context, 'pH 7.0', Icons.science_outlined));
|
||||
tableRows.add(_buildDataTableRow('MV', _getString(data, 'ph_7_mv')));
|
||||
tableRows.add(_buildDataTableRow('Before', _getString(data, 'ph_7_before')));
|
||||
tableRows.add(_buildDataTableRow('After', _getString(data, 'ph_7_after')));
|
||||
|
||||
contentWidgets.add(_buildCategoryHeader(context, 'pH 10.0', Icons.science_outlined));
|
||||
contentWidgets.add(_buildDataRow('MV', _getString(data, 'ph_10_mv')));
|
||||
contentWidgets.add(_buildDataRow('Before', _getString(data, 'ph_10_before')));
|
||||
contentWidgets.add(_buildDataRow('After', _getString(data, 'ph_10_after')));
|
||||
tableRows.add(_buildCategoryRow(context, 'pH 10.0', Icons.science_outlined));
|
||||
tableRows.add(_buildDataTableRow('MV', _getString(data, 'ph_10_mv')));
|
||||
tableRows.add(_buildDataTableRow('Before', _getString(data, 'ph_10_before')));
|
||||
tableRows.add(_buildDataTableRow('After', _getString(data, 'ph_10_after')));
|
||||
|
||||
contentWidgets.add(_buildCategoryHeader(context, 'Conductivity', Icons.thermostat));
|
||||
contentWidgets.add(_buildDataRow('Before', _getString(data, 'cond_before')));
|
||||
contentWidgets.add(_buildDataRow('After', _getString(data, 'cond_after')));
|
||||
tableRows.add(_buildCategoryRow(context, 'Conductivity', Icons.thermostat));
|
||||
tableRows.add(_buildDataTableRow('Before', _getString(data, 'cond_before')));
|
||||
tableRows.add(_buildDataTableRow('After', _getString(data, 'cond_after')));
|
||||
|
||||
contentWidgets.add(_buildCategoryHeader(context, 'Dissolved Oxygen', Icons.air));
|
||||
contentWidgets.add(_buildDataRow('Before', _getString(data, 'do_before')));
|
||||
contentWidgets.add(_buildDataRow('After', _getString(data, 'do_after')));
|
||||
tableRows.add(_buildCategoryRow(context, 'Dissolved Oxygen', Icons.air));
|
||||
tableRows.add(_buildDataTableRow('Before', _getString(data, 'do_before')));
|
||||
tableRows.add(_buildDataTableRow('After', _getString(data, 'do_after')));
|
||||
|
||||
contentWidgets.add(_buildCategoryHeader(context, 'Turbidity', Icons.waves));
|
||||
contentWidgets.add(_buildDataRow('0 NTU Before', _getString(data, 'turbidity_0_before')));
|
||||
contentWidgets.add(_buildDataRow('0 NTU After', _getString(data, 'turbidity_0_after')));
|
||||
contentWidgets.add(_buildDataRow('124 NTU Before', _getString(data, 'turbidity_124_before')));
|
||||
contentWidgets.add(_buildDataRow('124 NTU After', _getString(data, 'turbidity_124_after')));
|
||||
tableRows.add(_buildCategoryRow(context, 'Turbidity', Icons.waves));
|
||||
tableRows.add(_buildDataTableRow('0 NTU Before', _getString(data, 'turbidity_0_before')));
|
||||
tableRows.add(_buildDataTableRow('0 NTU After', _getString(data, 'turbidity_0_after')));
|
||||
tableRows.add(_buildDataTableRow('124 NTU Before', _getString(data, 'turbidity_124_before')));
|
||||
tableRows.add(_buildDataTableRow('124 NTU After', _getString(data, 'turbidity_124_after')));
|
||||
break;
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
addNestedMapRows(Map<String, dynamic>.from(data['vanDornChecks']));
|
||||
}
|
||||
contentWidgets.add(_buildDataRow('Comments', _getString(data, 'vanDornComments')));
|
||||
contentWidgets.add(_buildDataRow('Current Serial', _getString(data, 'vanDornCurrentSerial')));
|
||||
contentWidgets.add(_buildDataRow('New Serial', _getString(data, 'vanDornNewSerial')));
|
||||
tableRows.add(_buildDataTableRow('Comments', _getString(data, 'vanDornComments')));
|
||||
tableRows.add(_buildDataTableRow('Current Serial', _getString(data, 'vanDornCurrentSerial')));
|
||||
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) {
|
||||
addNestedMapRows(Map<String, dynamic>.from(data['vanDornReplacements']));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NPE Report':
|
||||
contentWidgets.add(_buildCategoryHeader(context, 'Event Info', Icons.calendar_today));
|
||||
contentWidgets.add(_buildDataRow('Date', _getString(data, 'eventDate')));
|
||||
contentWidgets.add(_buildDataRow('Time', _getString(data, 'eventTime')));
|
||||
contentWidgets.add(_buildDataRow('Sampler', _getString(data, 'firstSamplerName')));
|
||||
tableRows.add(_buildCategoryRow(context, 'Event Info', Icons.calendar_today));
|
||||
tableRows.add(_buildDataTableRow('Date', _getString(data, 'eventDate')));
|
||||
tableRows.add(_buildDataTableRow('Time', _getString(data, 'eventTime')));
|
||||
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) {
|
||||
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 {
|
||||
contentWidgets.add(_buildDataRow('Location', _getString(data, 'locationDescription')));
|
||||
contentWidgets.add(_buildDataRow('State', _getString(data, 'stateName')));
|
||||
tableRows.add(_buildDataTableRow('Location', _getString(data, 'locationDescription')));
|
||||
tableRows.add(_buildDataTableRow('State', _getString(data, 'stateName')));
|
||||
}
|
||||
contentWidgets.add(_buildDataRow('Latitude', _getString(data, 'latitude')));
|
||||
contentWidgets.add(_buildDataRow('Longitude', _getString(data, 'longitude')));
|
||||
tableRows.add(_buildDataTableRow('Latitude', _getString(data, 'latitude')));
|
||||
tableRows.add(_buildDataTableRow('Longitude', _getString(data, 'longitude')));
|
||||
|
||||
contentWidgets.add(_buildCategoryHeader(context, 'Parameters', Icons.bar_chart));
|
||||
contentWidgets.add(_buildDataRow('Oxygen Conc (mg/L)', _getString(data, 'oxygenConcentration')));
|
||||
contentWidgets.add(_buildDataRow('Oxygen Sat (%)', _getString(data, 'oxygenSaturation')));
|
||||
contentWidgets.add(_buildDataRow('pH', _getString(data, 'ph')));
|
||||
contentWidgets.add(_buildDataRow('Conductivity (µS/cm)', _getString(data, 'electricalConductivity')));
|
||||
contentWidgets.add(_buildDataRow('Temperature (°C)', _getString(data, 'temperature')));
|
||||
contentWidgets.add(_buildDataRow('Turbidity (NTU)', _getString(data, 'turbidity')));
|
||||
tableRows.add(_buildCategoryRow(context, 'Parameters', Icons.bar_chart));
|
||||
tableRows.add(_buildDataTableRow('Oxygen Conc (mg/L)', _getString(data, 'oxygenConcentration')));
|
||||
tableRows.add(_buildDataTableRow('Oxygen Sat (%)', _getString(data, 'oxygenSaturation')));
|
||||
tableRows.add(_buildDataTableRow('pH', _getString(data, 'ph')));
|
||||
tableRows.add(_buildDataTableRow('Conductivity (µS/cm)', _getString(data, 'electricalConductivity')));
|
||||
tableRows.add(_buildDataTableRow('Temperature (°C)', _getString(data, 'temperature')));
|
||||
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) {
|
||||
final observations = Map<String, bool>.from(data['fieldObservations']);
|
||||
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')));
|
||||
contentWidgets.add(_buildDataRow('Possible Source', _getString(data, 'possibleSource')));
|
||||
tableRows.add(_buildDataTableRow('Other Remarks', _getString(data, 'othersObservationRemark')));
|
||||
tableRows.add(_buildDataTableRow('Possible Source', _getString(data, 'possibleSource')));
|
||||
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;
|
||||
|
||||
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
|
||||
dialogContent = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: contentWidgets,
|
||||
// Assign the Table as the content for the 'else' block
|
||||
dialogContent = Table(
|
||||
columnWidths: const {
|
||||
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 ---
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
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("Selected Station ID: $stationId");
|
||||
|
||||
@ -47,7 +47,7 @@ class _InSituStep4SummaryState extends State<InSituStep4Summary> {
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
final marineLimits = authProvider.marineParameterLimits ?? [];
|
||||
final Set<String> invalidKeys = {};
|
||||
final int? stationId = widget.data.selectedStation?['station_id'];
|
||||
final int? stationId = widget.data.selectedStation?['man_station_id'];
|
||||
|
||||
final readings = {
|
||||
'oxygenConcentration': widget.data.oxygenConcentration,
|
||||
|
||||
@ -338,7 +338,11 @@ class LocalStorageService {
|
||||
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['submissionMessage'] = data.submissionMessage;
|
||||
|
||||
|
||||
@ -291,11 +291,19 @@ class MarineInSituSamplingService {
|
||||
// --- START FIX: Add ftpConfigId when queuing ---
|
||||
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
||||
|
||||
// --- MODIFIED: Use new data model methods for multi-json zip ---
|
||||
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,
|
||||
destinationDir: null, // Use temp dir
|
||||
);
|
||||
// --- END MODIFIED ---
|
||||
|
||||
if (dataZip != null) {
|
||||
// Queue for each config separately
|
||||
for (final config in ftpConfigs) {
|
||||
@ -401,7 +409,10 @@ class MarineInSituSamplingService {
|
||||
// Save/Update local log first
|
||||
if (savedLogPath != null && savedLogPath.isNotEmpty) {
|
||||
// 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();
|
||||
imageFiles.forEach((key, file) {
|
||||
logUpdateData[key] = file?.path; // Add paths back
|
||||
@ -439,12 +450,21 @@ class MarineInSituSamplingService {
|
||||
return {'success': true, 'message': successMessage, 'reportId': null}; // No report ID yet
|
||||
}
|
||||
|
||||
// --- START: MODIFIED _generateBaseFileName ---
|
||||
/// Helper to generate the base filename for ZIP files.
|
||||
String _generateBaseFileName(InSituSamplingData data) {
|
||||
final stationCode = data.selectedStation?['man_station_code'] ?? 'NA';
|
||||
|
||||
// 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.
|
||||
Future<Map<String, dynamic>> _generateAndUploadFtpFiles(InSituSamplingData data, Map<String, File> imageFiles, String serverName, String moduleName) async {
|
||||
@ -455,19 +475,32 @@ class MarineInSituSamplingService {
|
||||
module: 'marine',
|
||||
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;
|
||||
|
||||
if (localSubmissionDir != null && !await localSubmissionDir.exists()) {
|
||||
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(
|
||||
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,
|
||||
destinationDir: localSubmissionDir,
|
||||
);
|
||||
// --- END: MODIFIED createDataZip CALL ---
|
||||
|
||||
Map<String, dynamic> ftpDataResult = {'success': true, 'statuses': []};
|
||||
if (dataZip != null) {
|
||||
ftpDataResult = await _submissionFtpService.submit(
|
||||
@ -516,7 +549,10 @@ class MarineInSituSamplingService {
|
||||
final baseFileName = _generateBaseFileName(data); // Use helper
|
||||
|
||||
// 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();
|
||||
imageFileMap.forEach((key, file) {
|
||||
logMapData[key] = file?.path; // Store path or null
|
||||
@ -650,7 +686,9 @@ class MarineInSituSamplingService {
|
||||
final allLimits = await _dbHelper.loadMarineParameterLimits() ?? [];
|
||||
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
|
||||
|
||||
final readings = {
|
||||
@ -771,7 +809,7 @@ class MarineInSituSamplingService {
|
||||
if (isHit) {
|
||||
final valueStr = value.toStringAsFixed(5);
|
||||
final lowerStr = lowerLimit?.toStringAsFixed(5) ?? 'N/A';
|
||||
final upperStr = upperLimit?.toStringAsFixed(5) ?? 'N/A';
|
||||
final upperStr = upperLimit?.toStringAsFixed(5) ?? 'N/á';
|
||||
String limitStr;
|
||||
if (lowerStr != 'N/A' && upperStr != 'N/A') {
|
||||
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:usb_serial/usb_serial.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:intl/intl.dart'; // Import intl
|
||||
|
||||
import '../auth_provider.dart';
|
||||
import 'location_service.dart';
|
||||
@ -71,9 +72,10 @@ class MarineInvestigativeSamplingService {
|
||||
img.Image? originalImage = img.decodeImage(bytes);
|
||||
if (originalImage == null) return null;
|
||||
|
||||
// --- MODIFIED: Enforce landscape check logic from in-situ ---
|
||||
if (isRequired && originalImage.height > originalImage.width) {
|
||||
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}";
|
||||
@ -223,6 +225,10 @@ class MarineInvestigativeSamplingService {
|
||||
required AuthProvider authProvider,
|
||||
String? logDirectory,
|
||||
}) 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 imageFilesWithNulls = data.toApiImageFiles();
|
||||
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.");
|
||||
final baseFileNameForQueue = _generateBaseFileName(data);
|
||||
|
||||
// --- START FIX: Add ftpConfigId when queuing ---
|
||||
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
||||
|
||||
final dataZip = await _zippingService.createDataZip(
|
||||
@ -296,14 +301,13 @@ class MarineInvestigativeSamplingService {
|
||||
destinationDir: null, // Use temp dir
|
||||
);
|
||||
if (dataZip != null) {
|
||||
// Queue for each config separately
|
||||
for (final config in ftpConfigs) {
|
||||
final configId = config['ftp_config_id'];
|
||||
if (configId != null) {
|
||||
await _retryService.addFtpToQueue(
|
||||
localFilePath: 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
|
||||
);
|
||||
if (imageZip != null) {
|
||||
// Queue for each config separately
|
||||
for (final config in ftpConfigs) {
|
||||
final configId = config['ftp_config_id'];
|
||||
if (configId != null) {
|
||||
await _retryService.addFtpToQueue(
|
||||
localFilePath: 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}]};
|
||||
anyFtpSuccess = false;
|
||||
|
||||
} else {
|
||||
// Session is OK, proceed with normal FTP attempt
|
||||
try {
|
||||
// _generateAndUploadFtpFiles already uses the generic SubmissionFtpService
|
||||
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');
|
||||
} catch (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
|
||||
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);
|
||||
}
|
||||
|
||||
@ -397,9 +400,7 @@ class MarineInvestigativeSamplingService {
|
||||
|
||||
String? savedLogPath = logDirectory; // Use existing path if provided
|
||||
|
||||
// Save/Update local log first
|
||||
if (savedLogPath != null && savedLogPath.isNotEmpty) {
|
||||
// Prepare map with file paths for update
|
||||
Map<String, dynamic> logUpdateData = data.toDbJson();
|
||||
final imageFiles = data.toApiImageFiles();
|
||||
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.";
|
||||
// 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
|
||||
}
|
||||
|
||||
@ -444,11 +442,17 @@ class MarineInvestigativeSamplingService {
|
||||
} else if (data.stationTypeSelection == 'New Location') {
|
||||
stationCode = data.newStationCode ?? 'NEW_NA';
|
||||
}
|
||||
final datePart = data.samplingDate ?? 'NODATE';
|
||||
final timePart = (data.samplingTime ?? 'NOTIME').replaceAll(':', '-');
|
||||
final fileTimestamp = "${datePart}_${timePart}".replaceAll(' ', '_');
|
||||
|
||||
// --- START: MODIFIED (from in-situ) ---
|
||||
// Use reportId if available, otherwise fall back to timestamp
|
||||
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 ---
|
||||
}
|
||||
|
||||
|
||||
Future<Map<String, dynamic>> _generateAndUploadFtpFiles(MarineInvesManualSamplingData data, Map<String, File> imageFiles, String serverName, String moduleName) async {
|
||||
@ -459,7 +463,12 @@ class MarineInvestigativeSamplingService {
|
||||
module: 'marine',
|
||||
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;
|
||||
|
||||
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(
|
||||
jsonDataMap: {'db.json': jsonEncode(data.toDbJson())},
|
||||
baseFileName: baseFileName,
|
||||
destinationDir: localSubmissionDir,
|
||||
);
|
||||
// --- END: MODIFIED createDataZip call ---
|
||||
|
||||
Map<String, dynamic> ftpDataResult = {'success': true, 'statuses': []};
|
||||
if (dataZip != null) {
|
||||
ftpDataResult = await _submissionFtpService.submit(
|
||||
@ -525,13 +538,11 @@ class MarineInvestigativeSamplingService {
|
||||
data.submissionMessage = message;
|
||||
final baseFileName = _generateBaseFileName(data);
|
||||
|
||||
// Prepare log data map including file paths
|
||||
Map<String, dynamic> logMapData = data.toDbJson();
|
||||
final imageFileMap = data.toApiImageFiles();
|
||||
imageFileMap.forEach((key, file) {
|
||||
logMapData[key] = file?.path; // Store path or null
|
||||
});
|
||||
// Add submission metadata
|
||||
logMapData['submissionStatus'] = status;
|
||||
logMapData['submissionMessage'] = message;
|
||||
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 {
|
||||
|
||||
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)";
|
||||
|
||||
String stationName = 'N/A';
|
||||
@ -588,31 +600,54 @@ class MarineInvestigativeSamplingService {
|
||||
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()
|
||||
..writeln('🕵️ *Marine Investigative Sample $submissionType Submitted:*')
|
||||
..writeln()
|
||||
..writeln('*Station Name & Code:* $stationName ($stationCode)')
|
||||
..writeln('*Date of Submitted:* ${data.samplingDate}')
|
||||
..writeln('*Submitted by User:* ${data.firstSamplerName}')
|
||||
..writeln('*Date & Time of Submission:* $submissionDate $submissionTime')
|
||||
..writeln('*Submitted by User:* $submitter')
|
||||
..writeln('*Sonde ID:* ${data.sondeId ?? "N/A"}')
|
||||
..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
|
||||
..writeln()
|
||||
..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) {
|
||||
buffer.writeln('*Remarks for distance:* ${data.distanceDifferenceRemarks}');
|
||||
if (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A') {
|
||||
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();
|
||||
}
|
||||
// --- End internal function ---
|
||||
|
||||
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';
|
||||
|
||||
if (isSessionExpired) {
|
||||
@ -628,4 +663,161 @@ class MarineInvestigativeSamplingService {
|
||||
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};
|
||||
}
|
||||
|
||||
// --- START: MODIFIED _generateBaseFileName ---
|
||||
/// Helper to generate the base filename for ZIP files.
|
||||
String _generateBaseFileName(RiverInSituSamplingData data) {
|
||||
final stationCode = data.selectedStation?['sampling_station_code'] ?? 'UNKNOWN';
|
||||
|
||||
// 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.
|
||||
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 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;
|
||||
if (localSubmissionDir != null && !await localSubmissionDir.exists()) {
|
||||
await localSubmissionDir.create(recursive: true);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user