From 475e645d250d91cddd284298965cb73ebc268314 Mon Sep 17 00:00:00 2001 From: ALim Aidrus Date: Sun, 17 Aug 2025 22:01:37 +0800 Subject: [PATCH] fix marine tarball telegram alert --- lib/models/tarball_data.dart | 3 +- .../marine/manual/tarball_sampling_step1.dart | 150 +++++++++++++----- .../tarball_sampling_step3_summary.dart | 49 +++--- 3 files changed, 140 insertions(+), 62 deletions(-) diff --git a/lib/models/tarball_data.dart b/lib/models/tarball_data.dart index d1cc171..fbe9f21 100644 --- a/lib/models/tarball_data.dart +++ b/lib/models/tarball_data.dart @@ -1,3 +1,4 @@ +//import 'dart' as dart; import 'dart:io'; /// This class holds all the data collected across the multi-step tarball sampling form. @@ -97,7 +98,7 @@ class TarballSamplingData { 'optional_photo_remark_02': optionalRemark2 ?? '', 'optional_photo_remark_03': optionalRemark3 ?? '', 'optional_photo_remark_04': optionalRemark4 ?? '', - 'distance_difference_remarks': distanceDifferenceRemarks ?? '', + 'distance_remarks': distanceDifferenceRemarks ?? '', // Human-readable names for the Telegram alert 'tbl_station_name': selectedStation?['tbl_station_name']?.toString() ?? '', diff --git a/lib/screens/marine/manual/tarball_sampling_step1.dart b/lib/screens/marine/manual/tarball_sampling_step1.dart index 66bb186..0454ca6 100644 --- a/lib/screens/marine/manual/tarball_sampling_step1.dart +++ b/lib/screens/marine/manual/tarball_sampling_step1.dart @@ -17,6 +17,7 @@ class TarballSamplingStep1 extends StatefulWidget { class _TarballSamplingStep1State extends State { final _formKey = GlobalKey(); + // MODIFIED: A single data object is managed for the entire form. final _data = TarballSamplingData(); bool _isLoading = false; @@ -29,11 +30,10 @@ class _TarballSamplingStep1State extends State { final TextEditingController _currentLatController = TextEditingController(); final TextEditingController _currentLonController = TextEditingController(); - // --- State for Dropdowns and Location --- + // --- State for Dropdowns --- List _statesList = []; List _categoriesForState = []; List> _stationsForCategory = []; - double? _distanceDifference; @override void initState() { @@ -43,7 +43,6 @@ class _TarballSamplingStep1State extends State { @override void dispose() { - // Dispose all controllers to prevent memory leaks _firstSamplerController.dispose(); _dateController.dispose(); _timeController.dispose(); @@ -56,10 +55,6 @@ class _TarballSamplingStep1State extends State { void _initializeForm() { final auth = Provider.of(context, listen: false); - - // Set initial values for the data model and controllers - // This relies on the AuthProvider having been populated with data, - // which works offline if the data was fetched and cached previously. _data.firstSampler = auth.profileData?['first_name'] ?? 'Current User'; _data.firstSamplerUserId = auth.profileData?['user_id']; _firstSamplerController.text = _data.firstSampler!; @@ -70,8 +65,6 @@ class _TarballSamplingStep1State extends State { _dateController.text = _data.samplingDate!; _timeController.text = _data.samplingTime!; - // Populate the initial list of unique states from all available stations. - // This also relies on cached data in AuthProvider for offline use. final allStations = auth.tarballStations ?? []; if (allStations.isNotEmpty) { final states = allStations.map((s) => s['state_name'] as String?).whereType().toSet().toList(); @@ -80,12 +73,10 @@ class _TarballSamplingStep1State extends State { } } - /// Fetches the device's location with an offline-first approach. Future _getCurrentLocation() async { bool serviceEnabled; LocationPermission permission; - // Check if location services are enabled. serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { _showSnackBar('Location services are disabled. Please enable them.'); @@ -109,12 +100,7 @@ class _TarballSamplingStep1State extends State { setState(() => _isLoading = true); try { - // --- OFFLINE-FIRST LOGIC --- - // 1. Try to get the last known position. This is fast and works offline. Position? position = await Geolocator.getLastKnownPosition(); - - // 2. If no last known position, get the current position using GPS. - // This can work offline but may take longer. position ??= await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high); if (mounted) { @@ -144,22 +130,92 @@ class _TarballSamplingStep1State extends State { double distanceInMeters = Geolocator.distanceBetween(lat1, lon1, lat2, lon2); setState(() { - _distanceDifference = distanceInMeters / 1000; - _data.distanceDifference = _distanceDifference; + _data.distanceDifference = distanceInMeters / 1000; // Convert to km }); } } + // --- MODIFIED: This function now validates distance and shows a dialog if needed --- void _goToNextStep() { if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); - Navigator.push( - context, - MaterialPageRoute(builder: (context) => TarballSamplingStep2(data: _data)), - ); + final distanceInMeters = (_data.distanceDifference ?? 0) * 1000; + + if (distanceInMeters > 700) { + _showDistanceRemarkDialog(); + } else { + _data.distanceDifferenceRemarks = null; // Clear old remarks if within range + Navigator.push( + context, + MaterialPageRoute(builder: (context) => TarballSamplingStep2(data: _data)), + ); + } } } + // --- NEW: This function displays the mandatory remarks dialog --- + Future _showDistanceRemarkDialog() async { + final remarkController = TextEditingController(text: _data.distanceDifferenceRemarks); + final dialogFormKey = GlobalKey(); + + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Distance Warning'), + content: SingleChildScrollView( + child: Form( + key: dialogFormKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Your current location is more than 700m away from the station.'), + const SizedBox(height: 16), + TextFormField( + controller: remarkController, + decoration: const InputDecoration( + labelText: 'Remarks *', + hintText: 'Please provide a reason...', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Remarks are required to continue.'; + } + return null; + }, + maxLines: 3, + ), + ], + ), + ), + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(), + ), + FilledButton( + child: const Text('Confirm'), + onPressed: () { + if (dialogFormKey.currentState!.validate()) { + _data.distanceDifferenceRemarks = remarkController.text; + Navigator.of(context).pop(); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => TarballSamplingStep2(data: _data)), + ); + } + }, + ), + ], + ); + }, + ); + } + void _showSnackBar(String message) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); @@ -168,14 +224,10 @@ class _TarballSamplingStep1State extends State { @override Widget build(BuildContext context) { - // For offline functionality, all data used in the dropdowns (tarballStations, allUsers) - // must be fetched and cached in the AuthProvider when the app is online. final auth = Provider.of(context, listen: false); final allStations = auth.tarballStations ?? []; - - final currentUser = auth.profileData; final allUsers = auth.allUsers ?? []; - final secondSamplersList = allUsers.where((user) => user['user_id'] != currentUser?['user_id']).toList(); + final secondSamplersList = allUsers.where((user) => user['user_id'] != auth.profileData?['user_id']).toList(); return Scaffold( appBar: AppBar(title: const Text("Tarball Sampling (1/3)")), @@ -188,12 +240,10 @@ class _TarballSamplingStep1State extends State { const SizedBox(height: 24), TextFormField(controller: _firstSamplerController, readOnly: true, decoration: const InputDecoration(labelText: '1st Sampler')), const SizedBox(height: 16), - DropdownSearch>( - // --- CORRECTED: Added a ValueKey to prevent the "Duplicate GlobalKey" error --- - // This ensures the widget rebuilds cleanly when its item list changes. key: ValueKey(allUsers.length), items: secondSamplersList, + selectedItem: _data.secondSampler, // Bind to the data model itemAsString: (sampler) => "${sampler['first_name']} ${sampler['last_name']}", onChanged: (sampler) => setState(() => _data.secondSampler = sampler), popupProps: const PopupProps.menu( @@ -204,7 +254,6 @@ class _TarballSamplingStep1State extends State { dropdownSearchDecoration: InputDecoration(labelText: '2nd Sampler (Optional)'), ), ), - const SizedBox(height: 16), Row( children: [ @@ -216,6 +265,7 @@ class _TarballSamplingStep1State extends State { const SizedBox(height: 16), DropdownSearch( items: _statesList, + selectedItem: _data.selectedStateName, // Bind to the data model popupProps: const PopupProps.menu(showSearchBox: true, searchFieldProps: TextFieldProps(decoration: InputDecoration(hintText: "Search State..."))), dropdownDecoratorProps: const DropDownDecoratorProps(dropdownSearchDecoration: InputDecoration(labelText: "Select State *")), onChanged: (state) { @@ -225,7 +275,7 @@ class _TarballSamplingStep1State extends State { _data.selectedStation = null; _stationLatController.clear(); _stationLonController.clear(); - _distanceDifference = null; + _data.distanceDifference = null; if (state != null) { _categoriesForState = allStations.where((s) => s['state_name'] == state).map((s) => s['category_name'] as String?).whereType().toSet().toList(); @@ -241,6 +291,7 @@ class _TarballSamplingStep1State extends State { const SizedBox(height: 16), DropdownSearch( items: _categoriesForState, + selectedItem: _data.selectedCategoryName, // Bind to the data model enabled: _data.selectedStateName != null, popupProps: const PopupProps.menu(showSearchBox: true, searchFieldProps: TextFieldProps(decoration: InputDecoration(hintText: "Search Category..."))), dropdownDecoratorProps: const DropDownDecoratorProps(dropdownSearchDecoration: InputDecoration(labelText: "Select Category *")), @@ -250,7 +301,7 @@ class _TarballSamplingStep1State extends State { _data.selectedStation = null; _stationLatController.clear(); _stationLonController.clear(); - _distanceDifference = null; + _data.distanceDifference = null; if (category != null) { _stationsForCategory = allStations.where((s) => s['state_name'] == _data.selectedStateName && s['category_name'] == category).toList(); @@ -264,6 +315,7 @@ class _TarballSamplingStep1State extends State { const SizedBox(height: 16), DropdownSearch>( items: _stationsForCategory, + selectedItem: _data.selectedStation, // Bind to the data model enabled: _data.selectedCategoryName != null, itemAsString: (station) => "${station['tbl_station_code']} - ${station['tbl_station_name']}", popupProps: const PopupProps.menu(showSearchBox: true, searchFieldProps: TextFieldProps(decoration: InputDecoration(hintText: "Search Station..."))), @@ -289,14 +341,32 @@ class _TarballSamplingStep1State extends State { TextFormField(controller: _currentLatController, readOnly: true, decoration: const InputDecoration(labelText: 'Current Latitude')), const SizedBox(height: 16), TextFormField(controller: _currentLonController, readOnly: true, decoration: const InputDecoration(labelText: 'Current Longitude')), - if (_distanceDifference != null) + if (_data.distanceDifference != null) Padding( padding: const EdgeInsets.only(top: 16.0), - child: Text('Distance from Station: ${_distanceDifference!.toStringAsFixed(2)} km', - style: TextStyle( - fontWeight: FontWeight.bold, - color: _distanceDifference! > 1.0 ? Colors.red : Colors.green, - ) + // --- MODIFIED: This UI now better reflects the warning/ok status --- + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: ((_data.distanceDifference ?? 0) * 1000) > 700 ? Colors.red.withOpacity(0.1) : Colors.green.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ((_data.distanceDifference ?? 0) * 1000) > 700 ? Colors.red : Colors.green), + ), + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: Theme.of(context).textTheme.bodyLarge, + children: [ + const TextSpan(text: 'Distance from Station: '), + TextSpan( + text: '${(_data.distanceDifference! * 1000).toStringAsFixed(0)} meters', + style: TextStyle( + fontWeight: FontWeight.bold, + color: ((_data.distanceDifference ?? 0) * 1000) > 700 ? Colors.red : Colors.green), + ), + ], + ), + ), ), ), const SizedBox(height: 16), @@ -316,4 +386,4 @@ class _TarballSamplingStep1State extends State { ), ); } -} +} \ No newline at end of file diff --git a/lib/screens/marine/manual/tarball_sampling_step3_summary.dart b/lib/screens/marine/manual/tarball_sampling_step3_summary.dart index 703c613..a69d454 100644 --- a/lib/screens/marine/manual/tarball_sampling_step3_summary.dart +++ b/lib/screens/marine/manual/tarball_sampling_step3_summary.dart @@ -114,9 +114,11 @@ class _TarballSamplingStep3SummaryState extends State