From e90f4972cc0db33f1acbcabeae2f8106e1e071b0 Mon Sep 17 00:00:00 2001 From: ALim Aidrus Date: Sun, 10 Aug 2025 22:35:21 +0800 Subject: [PATCH] add in river flowrate menu under parameter sampling for river station --- lib/models/river_in_situ_sampling_data.dart | 31 ++- .../river_in_situ_step_3_data_capture.dart | 260 ++++++++++++++++-- .../widgets/river_in_situ_step_5_summary.dart | 38 ++- 3 files changed, 290 insertions(+), 39 deletions(-) diff --git a/lib/models/river_in_situ_sampling_data.dart b/lib/models/river_in_situ_sampling_data.dart index 47526ea..2a079cd 100644 --- a/lib/models/river_in_situ_sampling_data.dart +++ b/lib/models/river_in_situ_sampling_data.dart @@ -61,6 +61,15 @@ class RiverInSituSamplingData { double? tss; 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 --- String? submissionStatus; String? submissionMessage; @@ -71,15 +80,11 @@ class RiverInSituSamplingData { 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 json) { - // Helper function to safely create a File object from a path string File? fileFromJson(dynamic path) { return (path is String && path.isNotEmpty) ? File(path) : null; } - // Helper function to safely parse numbers double? doubleFromJson(dynamic value) { if (value is num) return value.toDouble(); if (value is String) return double.tryParse(value); @@ -130,7 +135,14 @@ class RiverInSituSamplingData { ..optionalImage1 = fileFromJson(json['r_man_optional_photo_01']) ..optionalImage2 = fileFromJson(json['r_man_optional_photo_02']) ..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_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 add('first_sampler_name', firstSamplerName); add('r_man_station_code', selectedStation?['sampling_station_code']); diff --git a/lib/screens/river/manual/widgets/river_in_situ_step_3_data_capture.dart b/lib/screens/river/manual/widgets/river_in_situ_step_3_data_capture.dart index 887d266..b84ac73 100644 --- a/lib/screens/river/manual/widgets/river_in_situ_step_3_data_capture.dart +++ b/lib/screens/river/manual/widgets/river_in_situ_step_3_data_capture.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; import 'package:usb_serial/usb_serial.dart'; +import 'package:intl/intl.dart'; import '../../../../models/river_in_situ_sampling_data.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 '../../../../serial/widget/serial_port_list_dialog.dart'; -// UPDATED: Class name changed from RiverInSituStep2DataCapture to RiverInSituStep3DataCapture class RiverInSituStep3DataCapture extends StatefulWidget { final RiverInSituSamplingData data; final VoidCallback onNext; @@ -25,11 +25,9 @@ class RiverInSituStep3DataCapture extends StatefulWidget { }); @override - // UPDATED: State class reference State createState() => _RiverInSituStep3DataCaptureState(); } -// UPDATED: State class name class _RiverInSituStep3DataCaptureState extends State { final _formKey = GlobalKey(); bool _isLoading = false; @@ -38,6 +36,7 @@ class _RiverInSituStep3DataCaptureState extends State> _parameters = []; + // Sonde parameter controllers final _sondeIdController = TextEditingController(); final _dateController = TextEditingController(); final _timeController = TextEditingController(); @@ -52,16 +51,26 @@ class _RiverInSituStep3DataCaptureState extends State _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 _handleConnectionAttempt(String type) async { final service = context.read(); final bool hasPermissions = await service.requestDevicePermissions(); @@ -268,6 +345,21 @@ class _RiverInSituStep3DataCaptureState extends State( + 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, + ), + ); + } +} diff --git a/lib/screens/river/manual/widgets/river_in_situ_step_5_summary.dart b/lib/screens/river/manual/widgets/river_in_situ_step_5_summary.dart index 1dc18a0..f336efb 100644 --- a/lib/screens/river/manual/widgets/river_in_situ_step_5_summary.dart +++ b/lib/screens/river/manual/widgets/river_in_situ_step_5_summary.dart @@ -46,7 +46,6 @@ class RiverInSituStep5Summary extends StatelessWidget { "${data.selectedStation?['sampling_station_code']} | ${data.selectedStation?['sampling_river']} | ${data.selectedStation?['sampling_basin']}" ), _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), const Divider(height: 20), - // ADDED: Display for Weather and Remarks. _buildDetailRow("Weather:", data.weather), _buildDetailRow("Event Remarks:", data.eventRemarks), _buildDetailRow("Lab Remarks:", data.labRemarks), const Divider(height: 20), - // UPDATED: Image cards reflect new names and data properties. _buildImageCard("Background Station", data.backgroundStationImage), _buildImageCard("Upstream River", data.upstreamRiverImage), _buildImageCard("Downstream River", data.downstreamRiverImage), - _buildImageCard("Sample Turbidity", data.sampleTurbidityImage), - - // REMOVED: pH paper image card. ], ), _buildSectionCard( 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 2", data.optionalImage2, remark: data.optionalRemark2), _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.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)), + + // ADDED: Display for Flowrate + const Divider(height: 20), + _buildFlowrateSummary(context), ], ), @@ -227,4 +229,24 @@ class RiverInSituStep5Summary extends StatelessWidget { ), ); } -} \ No newline at end of file + + // 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), + ], + ); + } +}