add in river flowrate menu under parameter sampling for river station

This commit is contained in:
ALim Aidrus 2025-08-10 22:35:21 +08:00
parent c7b97ecd1a
commit e90f4972cc
3 changed files with 290 additions and 39 deletions

View File

@ -61,6 +61,15 @@ class RiverInSituSamplingData {
double? tss; double? tss;
double? batteryVoltage; double? batteryVoltage;
// ADDED: New properties for Flowrate
String? flowrateMethod; // 'Surface Drifter', 'Flowmeter', 'NA'
double? flowrateSurfaceDrifterHeight;
double? flowrateSurfaceDrifterDistance;
String? flowrateSurfaceDrifterTimeFirst;
String? flowrateSurfaceDrifterTimeLast;
double? flowrateValue;
// --- Post-Submission Status --- // --- Post-Submission Status ---
String? submissionStatus; String? submissionStatus;
String? submissionMessage; String? submissionMessage;
@ -71,15 +80,11 @@ class RiverInSituSamplingData {
this.samplingTime, this.samplingTime,
}); });
/// ADDED: Factory constructor to create an instance from a map (JSON).
/// This is the required fix for the "fromJson isn't defined" error.
factory RiverInSituSamplingData.fromJson(Map<String, dynamic> json) { factory RiverInSituSamplingData.fromJson(Map<String, dynamic> json) {
// Helper function to safely create a File object from a path string
File? fileFromJson(dynamic path) { File? fileFromJson(dynamic path) {
return (path is String && path.isNotEmpty) ? File(path) : null; return (path is String && path.isNotEmpty) ? File(path) : null;
} }
// Helper function to safely parse numbers
double? doubleFromJson(dynamic value) { double? doubleFromJson(dynamic value) {
if (value is num) return value.toDouble(); if (value is num) return value.toDouble();
if (value is String) return double.tryParse(value); if (value is String) return double.tryParse(value);
@ -130,7 +135,14 @@ class RiverInSituSamplingData {
..optionalImage1 = fileFromJson(json['r_man_optional_photo_01']) ..optionalImage1 = fileFromJson(json['r_man_optional_photo_01'])
..optionalImage2 = fileFromJson(json['r_man_optional_photo_02']) ..optionalImage2 = fileFromJson(json['r_man_optional_photo_02'])
..optionalImage3 = fileFromJson(json['r_man_optional_photo_03']) ..optionalImage3 = fileFromJson(json['r_man_optional_photo_03'])
..optionalImage4 = fileFromJson(json['r_man_optional_photo_04']); ..optionalImage4 = fileFromJson(json['r_man_optional_photo_04'])
// ADDED: Flowrate fields from JSON
..flowrateMethod = json['r_man_flowrate_method']
..flowrateSurfaceDrifterHeight = doubleFromJson(json['r_man_flowrate_sd_height'])
..flowrateSurfaceDrifterDistance = doubleFromJson(json['r_man_flowrate_sd_distance'])
..flowrateSurfaceDrifterTimeFirst = json['r_man_flowrate_sd_time_first']
..flowrateSurfaceDrifterTimeLast = json['r_man_flowrate_sd_time_last']
..flowrateValue = doubleFromJson(json['r_man_flowrate_value']);
} }
@ -183,6 +195,15 @@ class RiverInSituSamplingData {
add('r_man_tss', tss); add('r_man_tss', tss);
add('r_man_battery_volt', batteryVoltage); add('r_man_battery_volt', batteryVoltage);
// ADDED: Flowrate fields to API form data
add('r_man_flowrate_method', flowrateMethod);
add('r_man_flowrate_sd_height', flowrateSurfaceDrifterHeight);
add('r_man_flowrate_sd_distance', flowrateSurfaceDrifterDistance);
add('r_man_flowrate_sd_time_first', flowrateSurfaceDrifterTimeFirst);
add('r_man_flowrate_sd_time_last', flowrateSurfaceDrifterTimeLast);
add('r_man_flowrate_value', flowrateValue);
// Additional data for display or logging // Additional data for display or logging
add('first_sampler_name', firstSamplerName); add('first_sampler_name', firstSamplerName);
add('r_man_station_code', selectedStation?['sampling_station_code']); add('r_man_station_code', selectedStation?['sampling_station_code']);

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.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:intl/intl.dart';
import '../../../../models/river_in_situ_sampling_data.dart'; import '../../../../models/river_in_situ_sampling_data.dart';
import '../../../../services/river_in_situ_sampling_service.dart'; import '../../../../services/river_in_situ_sampling_service.dart';
@ -13,7 +14,6 @@ import '../../../../serial/serial_manager.dart';
import '../../../../bluetooth/widgets/bluetooth_device_list_dialog.dart'; import '../../../../bluetooth/widgets/bluetooth_device_list_dialog.dart';
import '../../../../serial/widget/serial_port_list_dialog.dart'; import '../../../../serial/widget/serial_port_list_dialog.dart';
// UPDATED: Class name changed from RiverInSituStep2DataCapture to RiverInSituStep3DataCapture
class RiverInSituStep3DataCapture extends StatefulWidget { class RiverInSituStep3DataCapture extends StatefulWidget {
final RiverInSituSamplingData data; final RiverInSituSamplingData data;
final VoidCallback onNext; final VoidCallback onNext;
@ -25,11 +25,9 @@ class RiverInSituStep3DataCapture extends StatefulWidget {
}); });
@override @override
// UPDATED: State class reference
State<RiverInSituStep3DataCapture> createState() => _RiverInSituStep3DataCaptureState(); State<RiverInSituStep3DataCapture> createState() => _RiverInSituStep3DataCaptureState();
} }
// UPDATED: State class name
class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCapture> { class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCapture> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
bool _isLoading = false; bool _isLoading = false;
@ -38,6 +36,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
final List<Map<String, dynamic>> _parameters = []; final List<Map<String, dynamic>> _parameters = [];
// Sonde parameter controllers
final _sondeIdController = TextEditingController(); final _sondeIdController = TextEditingController();
final _dateController = TextEditingController(); final _dateController = TextEditingController();
final _timeController = TextEditingController(); final _timeController = TextEditingController();
@ -52,16 +51,26 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
final _tssController = TextEditingController(); final _tssController = TextEditingController();
final _batteryController = TextEditingController(); final _batteryController = TextEditingController();
// ADDED: Flowrate controllers and state
String? _selectedFlowrateMethod;
final _flowrateValueController = TextEditingController();
final _sdHeightController = TextEditingController();
final _sdDistanceController = TextEditingController();
final _sdTimeFirstController = TextEditingController();
final _sdTimeLastController = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initializeControllers(); _initializeControllers();
_initializeFlowrateControllers();
} }
@override @override
void dispose() { void dispose() {
_dataSubscription?.cancel(); _dataSubscription?.cancel();
_disposeControllers(); _disposeControllers();
_disposeFlowrateControllers();
super.dispose(); super.dispose();
} }
@ -73,27 +82,16 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
_dateController.text = widget.data.dataCaptureDate ?? ''; _dateController.text = widget.data.dataCaptureDate ?? '';
_timeController.text = widget.data.dataCaptureTime ?? ''; _timeController.text = widget.data.dataCaptureTime ?? '';
widget.data.oxygenConcentration ??= -999.0; _oxyConcController.text = widget.data.oxygenConcentration?.toString() ?? '-999.0';
widget.data.oxygenSaturation ??= -999.0; _oxySatController.text = widget.data.oxygenSaturation?.toString() ?? '-999.0';
widget.data.ph ??= -999.0; _phController.text = widget.data.ph?.toString() ?? '-999.0';
widget.data.salinity ??= -999.0; _salinityController.text = widget.data.salinity?.toString() ?? '-999.0';
widget.data.electricalConductivity ??= -999.0; _ecController.text = widget.data.electricalConductivity?.toString() ?? '-999.0';
widget.data.temperature ??= -999.0; _tempController.text = widget.data.temperature?.toString() ?? '-999.0';
widget.data.tds ??= -999.0; _tdsController.text = widget.data.tds?.toString() ?? '-999.0';
widget.data.turbidity ??= -999.0; _turbidityController.text = widget.data.turbidity?.toString() ?? '-999.0';
widget.data.tss ??= -999.0; _tssController.text = widget.data.tss?.toString() ?? '-999.0';
widget.data.batteryVoltage ??= -999.0; _batteryController.text = widget.data.batteryVoltage?.toString() ?? '-999.0';
_oxyConcController.text = widget.data.oxygenConcentration!.toString();
_oxySatController.text = widget.data.oxygenSaturation!.toString();
_phController.text = widget.data.ph!.toString();
_salinityController.text = widget.data.salinity!.toString();
_ecController.text = widget.data.electricalConductivity!.toString();
_tempController.text = widget.data.temperature!.toString();
_tdsController.text = widget.data.tds!.toString();
_turbidityController.text = widget.data.turbidity!.toString();
_tssController.text = widget.data.tss!.toString();
_batteryController.text = widget.data.batteryVoltage!.toString();
if (_parameters.isEmpty) { if (_parameters.isEmpty) {
_parameters.addAll([ _parameters.addAll([
@ -127,6 +125,85 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
_batteryController.dispose(); _batteryController.dispose();
} }
// --- START: Flowrate Logic ---
void _initializeFlowrateControllers() {
_selectedFlowrateMethod = widget.data.flowrateMethod;
_flowrateValueController.text = widget.data.flowrateValue?.toString() ?? '';
_sdHeightController.text = widget.data.flowrateSurfaceDrifterHeight?.toString() ?? '';
_sdDistanceController.text = widget.data.flowrateSurfaceDrifterDistance?.toString() ?? '';
_sdTimeFirstController.text = widget.data.flowrateSurfaceDrifterTimeFirst ?? '';
_sdTimeLastController.text = widget.data.flowrateSurfaceDrifterTimeLast ?? '';
}
void _disposeFlowrateControllers() {
_flowrateValueController.dispose();
_sdHeightController.dispose();
_sdDistanceController.dispose();
_sdTimeFirstController.dispose();
_sdTimeLastController.dispose();
}
void _onFlowrateMethodChanged(String? value) {
setState(() {
_selectedFlowrateMethod = value;
if (value == 'NA') {
_flowrateValueController.text = 'NA';
} else if (value == 'Flowmeter') {
_flowrateValueController.clear();
} else {
// Clear calculated value when switching back to Surface Drifter
_flowrateValueController.clear();
}
});
}
void _calculateFlowrate() {
final distance = double.tryParse(_sdDistanceController.text);
final timeFirstStr = _sdTimeFirstController.text;
final timeLastStr = _sdTimeLastController.text;
if (distance == null || timeFirstStr.isEmpty || timeLastStr.isEmpty) {
_showSnackBar("Please fill in Distance, Time First, and Time Last.", isError: true);
return;
}
try {
final timeFormat = DateFormat("HH:mm:ss");
final timeFirst = timeFormat.parse(timeFirstStr);
final timeLast = timeFormat.parse(timeLastStr);
final differenceInSeconds = timeLast.difference(timeFirst).inSeconds;
if (differenceInSeconds <= 0) {
_showSnackBar("Time Last Deploy must be after Time First Deploy.", isError: true);
return;
}
final flowrate = distance / differenceInSeconds;
setState(() {
_flowrateValueController.text = flowrate.toStringAsFixed(4);
});
} catch (e) {
_showSnackBar("Invalid time format. Please use HH:mm:ss.", isError: true);
}
}
Future<void> _selectTime(BuildContext context, TextEditingController controller) async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (picked != null) {
final now = DateTime.now();
final dt = DateTime(now.year, now.month, now.day, picked.hour, picked.minute);
setState(() {
controller.text = DateFormat('HH:mm:ss').format(dt);
});
}
}
// --- END: Flowrate Logic ---
Future<void> _handleConnectionAttempt(String type) async { Future<void> _handleConnectionAttempt(String type) async {
final service = context.read<RiverInSituSamplingService>(); final service = context.read<RiverInSituSamplingService>();
final bool hasPermissions = await service.requestDevicePermissions(); final bool hasPermissions = await service.requestDevicePermissions();
@ -268,6 +345,21 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
widget.data.turbidity = double.tryParse(_turbidityController.text) ?? defaultValue; widget.data.turbidity = double.tryParse(_turbidityController.text) ?? defaultValue;
widget.data.tss = double.tryParse(_tssController.text) ?? defaultValue; widget.data.tss = double.tryParse(_tssController.text) ?? defaultValue;
widget.data.batteryVoltage = double.tryParse(_batteryController.text) ?? defaultValue; widget.data.batteryVoltage = double.tryParse(_batteryController.text) ?? defaultValue;
// Save flowrate data
widget.data.flowrateMethod = _selectedFlowrateMethod;
if (_selectedFlowrateMethod == 'Surface Drifter') {
widget.data.flowrateSurfaceDrifterHeight = double.tryParse(_sdHeightController.text);
widget.data.flowrateSurfaceDrifterDistance = double.tryParse(_sdDistanceController.text);
widget.data.flowrateSurfaceDrifterTimeFirst = _sdTimeFirstController.text;
widget.data.flowrateSurfaceDrifterTimeLast = _sdTimeLastController.text;
widget.data.flowrateValue = double.tryParse(_flowrateValueController.text);
} else if (_selectedFlowrateMethod == 'Flowmeter') {
widget.data.flowrateValue = double.tryParse(_flowrateValueController.text);
} else { // NA
widget.data.flowrateValue = null;
}
} catch (e) { } catch (e) {
_showSnackBar("Could not save parameters due to a data format error.", isError: true); _showSnackBar("Could not save parameters due to a data format error.", isError: true);
return; return;
@ -353,7 +445,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
valueListenable: service.sondeId, valueListenable: service.sondeId,
builder: (context, sondeId, child) { builder: (context, sondeId, child) {
final newSondeId = sondeId ?? ''; final newSondeId = sondeId ?? '';
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && _sondeIdController.text != newSondeId) { if (mounted && _sondeIdController.text != newSondeId) {
_sondeIdController.text = newSondeId; _sondeIdController.text = newSondeId;
@ -395,6 +486,11 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
}).toList(), }).toList(),
), ),
const Divider(height: 32),
// --- START: Flowrate Section ---
_buildFlowrateSection(),
// --- END: Flowrate Section ---
const SizedBox(height: 32), const SizedBox(height: 32),
ElevatedButton( ElevatedButton(
onPressed: _goToNextStep, onPressed: _goToNextStep,
@ -476,4 +572,116 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
), ),
); );
} }
// ADDED: Widget for the entire Flowrate section
Widget _buildFlowrateSection() {
return Card(
margin: const EdgeInsets.symmetric(vertical: 4.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Flowrate", style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildFlowrateRadioButton("Surface Drifter"),
_buildFlowrateRadioButton("Flowmeter"),
_buildFlowrateRadioButton("NA"),
],
),
if (_selectedFlowrateMethod == 'Surface Drifter')
_buildSurfaceDrifterFields(),
if (_selectedFlowrateMethod == 'Flowmeter')
_buildFlowmeterField(),
if (_selectedFlowrateMethod == 'NA')
_buildNAField(),
],
),
),
);
}
Widget _buildFlowrateRadioButton(String title) {
return Column(
children: [
Radio<String>(
value: title,
groupValue: _selectedFlowrateMethod,
onChanged: _onFlowrateMethodChanged,
),
Text(title),
],
);
}
Widget _buildSurfaceDrifterFields() {
return Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Column(
children: [
TextFormField(
controller: _sdHeightController,
decoration: const InputDecoration(labelText: 'Height (m)'),
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
TextFormField(
controller: _sdDistanceController,
decoration: const InputDecoration(labelText: 'Distance (m)'),
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
TextFormField(
controller: _sdTimeFirstController,
decoration: const InputDecoration(labelText: 'Time First Deploy (HH:mm:ss)', suffixIcon: Icon(Icons.timer)),
readOnly: true,
onTap: () => _selectTime(context, _sdTimeFirstController),
),
const SizedBox(height: 16),
TextFormField(
controller: _sdTimeLastController,
decoration: const InputDecoration(labelText: 'Time Last Deploy (HH:mm:ss)', suffixIcon: Icon(Icons.timer)),
readOnly: true,
onTap: () => _selectTime(context, _sdTimeLastController),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _calculateFlowrate,
child: const Text('Get Flowrate'),
),
const SizedBox(height: 16),
TextFormField(
controller: _flowrateValueController,
decoration: const InputDecoration(labelText: 'Flowrate (m/s)'),
readOnly: true,
),
],
),
);
}
Widget _buildFlowmeterField() {
return Padding(
padding: const EdgeInsets.only(top: 16.0),
child: TextFormField(
controller: _flowrateValueController,
decoration: const InputDecoration(labelText: 'Flowrate (m/s)'),
keyboardType: TextInputType.number,
),
);
}
Widget _buildNAField() {
return Padding(
padding: const EdgeInsets.only(top: 16.0),
child: TextFormField(
controller: _flowrateValueController,
decoration: const InputDecoration(labelText: 'Flowrate (m/s)'),
readOnly: true,
),
);
}
} }

View File

@ -46,7 +46,6 @@ class RiverInSituStep5Summary extends StatelessWidget {
"${data.selectedStation?['sampling_station_code']} | ${data.selectedStation?['sampling_river']} | ${data.selectedStation?['sampling_basin']}" "${data.selectedStation?['sampling_station_code']} | ${data.selectedStation?['sampling_river']} | ${data.selectedStation?['sampling_basin']}"
), ),
_buildDetailRow("Station Location:", "${data.stationLatitude}, ${data.stationLongitude}"), _buildDetailRow("Station Location:", "${data.stationLatitude}, ${data.stationLongitude}"),
// REMOVED: Weather and remarks moved to the next section.
], ],
), ),
@ -60,26 +59,25 @@ class RiverInSituStep5Summary extends StatelessWidget {
_buildDetailRow("Distance Remarks:", data.distanceDifferenceRemarks), _buildDetailRow("Distance Remarks:", data.distanceDifferenceRemarks),
const Divider(height: 20), const Divider(height: 20),
// ADDED: Display for Weather and Remarks.
_buildDetailRow("Weather:", data.weather), _buildDetailRow("Weather:", data.weather),
_buildDetailRow("Event Remarks:", data.eventRemarks), _buildDetailRow("Event Remarks:", data.eventRemarks),
_buildDetailRow("Lab Remarks:", data.labRemarks), _buildDetailRow("Lab Remarks:", data.labRemarks),
const Divider(height: 20), const Divider(height: 20),
// UPDATED: Image cards reflect new names and data properties.
_buildImageCard("Background Station", data.backgroundStationImage), _buildImageCard("Background Station", data.backgroundStationImage),
_buildImageCard("Upstream River", data.upstreamRiverImage), _buildImageCard("Upstream River", data.upstreamRiverImage),
_buildImageCard("Downstream River", data.downstreamRiverImage), _buildImageCard("Downstream River", data.downstreamRiverImage),
_buildImageCard("Sample Turbidity", data.sampleTurbidityImage),
// REMOVED: pH paper image card.
], ],
), ),
_buildSectionCard( _buildSectionCard(
context, context,
"Optional Photos & Remarks", "Additional Photos & Remarks",
[ [
_buildImageCard("Sample Turbidity", data.sampleTurbidityImage),
const Divider(height: 24),
Text("Optional Photos", style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
_buildImageCard("Optional Photo 1", data.optionalImage1, remark: data.optionalRemark1), _buildImageCard("Optional Photo 1", data.optionalImage1, remark: data.optionalRemark1),
_buildImageCard("Optional Photo 2", data.optionalImage2, remark: data.optionalRemark2), _buildImageCard("Optional Photo 2", data.optionalImage2, remark: data.optionalRemark2),
_buildImageCard("Optional Photo 3", data.optionalImage3, remark: data.optionalRemark3), _buildImageCard("Optional Photo 3", data.optionalImage3, remark: data.optionalRemark3),
@ -104,6 +102,10 @@ class RiverInSituStep5Summary extends StatelessWidget {
_buildParameterListItem(context, icon: Icons.opacity, label: "Turbidity", unit: "NTU", value: data.turbidity?.toStringAsFixed(2)), _buildParameterListItem(context, icon: Icons.opacity, label: "Turbidity", unit: "NTU", value: data.turbidity?.toStringAsFixed(2)),
_buildParameterListItem(context, icon: Icons.filter_alt_outlined, label: "TSS", unit: "mg/L", value: data.tss?.toStringAsFixed(2)), _buildParameterListItem(context, icon: Icons.filter_alt_outlined, label: "TSS", unit: "mg/L", value: data.tss?.toStringAsFixed(2)),
_buildParameterListItem(context, icon: Icons.battery_charging_full, label: "Battery", unit: "V", value: data.batteryVoltage?.toStringAsFixed(2)), _buildParameterListItem(context, icon: Icons.battery_charging_full, label: "Battery", unit: "V", value: data.batteryVoltage?.toStringAsFixed(2)),
// ADDED: Display for Flowrate
const Divider(height: 20),
_buildFlowrateSummary(context),
], ],
), ),
@ -227,4 +229,24 @@ class RiverInSituStep5Summary extends StatelessWidget {
), ),
); );
} }
// ADDED: Widget to build the flowrate summary section
Widget _buildFlowrateSummary(BuildContext context) {
final method = data.flowrateMethod ?? 'N/A';
final value = data.flowrateValue != null ? '${data.flowrateValue!.toStringAsFixed(4)} m/s' : 'NA';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow("Flowrate Method:", method),
if (method == 'Surface Drifter') ...[
_buildDetailRow(" Height:", data.flowrateSurfaceDrifterHeight?.toString()),
_buildDetailRow(" Distance:", data.flowrateSurfaceDrifterDistance?.toString()),
_buildDetailRow(" Time First:", data.flowrateSurfaceDrifterTimeFirst),
_buildDetailRow(" Time Last:", data.flowrateSurfaceDrifterTimeLast),
],
_buildDetailRow("Flowrate Value:", value),
],
);
}
} }