fix marine tarball telegram alert
This commit is contained in:
parent
e3b58bf74e
commit
475e645d25
@ -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() ?? '',
|
||||
|
||||
@ -17,6 +17,7 @@ class TarballSamplingStep1 extends StatefulWidget {
|
||||
|
||||
class _TarballSamplingStep1State extends State<TarballSamplingStep1> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
// 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<TarballSamplingStep1> {
|
||||
final TextEditingController _currentLatController = TextEditingController();
|
||||
final TextEditingController _currentLonController = TextEditingController();
|
||||
|
||||
// --- State for Dropdowns and Location ---
|
||||
// --- State for Dropdowns ---
|
||||
List<String> _statesList = [];
|
||||
List<String> _categoriesForState = [];
|
||||
List<Map<String, dynamic>> _stationsForCategory = [];
|
||||
double? _distanceDifference;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -43,7 +43,6 @@ class _TarballSamplingStep1State extends State<TarballSamplingStep1> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Dispose all controllers to prevent memory leaks
|
||||
_firstSamplerController.dispose();
|
||||
_dateController.dispose();
|
||||
_timeController.dispose();
|
||||
@ -56,10 +55,6 @@ class _TarballSamplingStep1State extends State<TarballSamplingStep1> {
|
||||
|
||||
void _initializeForm() {
|
||||
final auth = Provider.of<AuthProvider>(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<TarballSamplingStep1> {
|
||||
_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<String>().toSet().toList();
|
||||
@ -80,12 +73,10 @@ class _TarballSamplingStep1State extends State<TarballSamplingStep1> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches the device's location with an offline-first approach.
|
||||
Future<void> _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<TarballSamplingStep1> {
|
||||
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<TarballSamplingStep1> {
|
||||
|
||||
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<void> _showDistanceRemarkDialog() async {
|
||||
final remarkController = TextEditingController(text: _data.distanceDifferenceRemarks);
|
||||
final dialogFormKey = GlobalKey<FormState>();
|
||||
|
||||
return showDialog<void>(
|
||||
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: <Widget>[
|
||||
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<TarballSamplingStep1> {
|
||||
|
||||
@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<AuthProvider>(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<TarballSamplingStep1> {
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(controller: _firstSamplerController, readOnly: true, decoration: const InputDecoration(labelText: '1st Sampler')),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
DropdownSearch<Map<String, dynamic>>(
|
||||
// --- 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<TarballSamplingStep1> {
|
||||
dropdownSearchDecoration: InputDecoration(labelText: '2nd Sampler (Optional)'),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
@ -216,6 +265,7 @@ class _TarballSamplingStep1State extends State<TarballSamplingStep1> {
|
||||
const SizedBox(height: 16),
|
||||
DropdownSearch<String>(
|
||||
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<TarballSamplingStep1> {
|
||||
_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<String>().toSet().toList();
|
||||
@ -241,6 +291,7 @@ class _TarballSamplingStep1State extends State<TarballSamplingStep1> {
|
||||
const SizedBox(height: 16),
|
||||
DropdownSearch<String>(
|
||||
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<TarballSamplingStep1> {
|
||||
_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<TarballSamplingStep1> {
|
||||
const SizedBox(height: 16),
|
||||
DropdownSearch<Map<String, dynamic>>(
|
||||
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<TarballSamplingStep1> {
|
||||
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: <TextSpan>[
|
||||
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<TarballSamplingStep1> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -114,9 +114,11 @@ class _TarballSamplingStep3SummaryState extends State<TarballSamplingStep3Summar
|
||||
_buildDetailRow("Current Longitude:", widget.data.currentLongitude),
|
||||
_buildDetailRow("Distance Difference:",
|
||||
widget.data.distanceDifference != null
|
||||
? "${widget.data.distanceDifference!.toStringAsFixed(2)} km"
|
||||
? "${(widget.data.distanceDifference! * 1000).toStringAsFixed(0)} meters"
|
||||
: "N/A"
|
||||
),
|
||||
// NECESSARY CHANGE: Add this line to display the remarks.
|
||||
_buildDetailRow("Distance Remarks:", widget.data.distanceDifferenceRemarks),
|
||||
],
|
||||
),
|
||||
|
||||
@ -199,27 +201,32 @@ class _TarballSamplingStep3SummaryState extends State<TarballSamplingStep3Summar
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String? value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
// This function now correctly handles null or empty remarks by displaying 'N/A'
|
||||
final bool isValueAvailable = value != null && value.isNotEmpty;
|
||||
return Visibility(
|
||||
visible: isValueAvailable, // Only show the row if there is a value to display
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
value != null && value.isNotEmpty ? value : 'N/A',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
value ?? 'N/A', // Display the value or 'N/A' if null
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -257,4 +264,4 @@ class _TarballSamplingStep3SummaryState extends State<TarballSamplingStep3Summar
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user