repair on the register screen
This commit is contained in:
parent
f44245fb5a
commit
31b64fc203
@ -27,9 +27,9 @@
|
|||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
<!-- END: STORAGE PERMISSIONS -->
|
<!-- END: STORAGE PERMISSIONS -->
|
||||||
|
|
||||||
<!-- MMS V4 1.2.05 -->
|
<!-- MMS V4 1.2.06 -->
|
||||||
<application
|
<application
|
||||||
android:label="MMS V4 debug"
|
android:label="MMS V4 1.2.07"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:requestLegacyExternalStorage="true">
|
android:requestLegacyExternalStorage="true">
|
||||||
|
|||||||
@ -267,6 +267,27 @@ class AuthProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: NEW METHOD FOR REGISTRATION SCREEN ---
|
||||||
|
Future<void> syncRegistrationData() async {
|
||||||
|
final connectivityResult = await Connectivity().checkConnectivity();
|
||||||
|
if (connectivityResult.contains(ConnectivityResult.none)) {
|
||||||
|
debugPrint("AuthProvider: Device is OFFLINE. Skipping registration data sync.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint("AuthProvider: Fetching data for registration screen...");
|
||||||
|
final result = await _apiService.syncRegistrationData();
|
||||||
|
|
||||||
|
if (result['success']) {
|
||||||
|
await _loadDataFromCache(); // Reload data from DB into the provider
|
||||||
|
notifyListeners(); // Notify the UI to rebuild
|
||||||
|
debugPrint("AuthProvider: Registration data loaded and UI notified.");
|
||||||
|
} else {
|
||||||
|
debugPrint("AuthProvider: Registration data sync failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END: NEW METHOD FOR REGISTRATION SCREEN ---
|
||||||
|
|
||||||
Future<void> refreshProfile() async {
|
Future<void> refreshProfile() async {
|
||||||
final connectivityResult = await Connectivity().checkConnectivity();
|
final connectivityResult = await Connectivity().checkConnectivity();
|
||||||
if (connectivityResult.contains(ConnectivityResult.none)) {
|
if (connectivityResult.contains(ConnectivityResult.none)) {
|
||||||
|
|||||||
@ -8,9 +8,7 @@ import 'package:simple_barcode_scanner/simple_barcode_scanner.dart';
|
|||||||
|
|
||||||
import '../../../../auth_provider.dart';
|
import '../../../../auth_provider.dart';
|
||||||
import '../../../../models/in_situ_sampling_data.dart';
|
import '../../../../models/in_situ_sampling_data.dart';
|
||||||
// START CHANGE: Import the new, correct service file
|
|
||||||
import '../../../../services/marine_in_situ_sampling_service.dart';
|
import '../../../../services/marine_in_situ_sampling_service.dart';
|
||||||
// END CHANGE
|
|
||||||
|
|
||||||
class InSituStep1SamplingInfo extends StatefulWidget {
|
class InSituStep1SamplingInfo extends StatefulWidget {
|
||||||
final InSituSamplingData data;
|
final InSituSamplingData data;
|
||||||
@ -90,6 +88,10 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
_dateController.text = widget.data.samplingDate!;
|
_dateController.text = widget.data.samplingDate!;
|
||||||
_timeController.text = widget.data.samplingTime!;
|
_timeController.text = widget.data.samplingTime!;
|
||||||
|
|
||||||
|
if (widget.data.samplingType == null) {
|
||||||
|
widget.data.samplingType = 'Schedule';
|
||||||
|
}
|
||||||
|
|
||||||
final allStations = auth.manualStations ?? [];
|
final allStations = auth.manualStations ?? [];
|
||||||
if (allStations.isNotEmpty) {
|
if (allStations.isNotEmpty) {
|
||||||
final states = allStations.map((s) => s['state_name'] as String?).whereType<String>().toSet().toList();
|
final states = allStations.map((s) => s['state_name'] as String?).whereType<String>().toSet().toList();
|
||||||
@ -110,7 +112,8 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
.where((s) =>
|
.where((s) =>
|
||||||
s['state_name'] == widget.data.selectedStateName &&
|
s['state_name'] == widget.data.selectedStateName &&
|
||||||
s['category_name'] == widget.data.selectedCategoryName)
|
s['category_name'] == widget.data.selectedCategoryName)
|
||||||
.toList();
|
.toList()
|
||||||
|
..sort((a, b) => (a['man_station_code'] ?? '').compareTo(b['man_station_code'] ?? ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -121,9 +124,7 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
|
|
||||||
Future<void> _getCurrentLocation() async {
|
Future<void> _getCurrentLocation() async {
|
||||||
setState(() => _isLoadingLocation = true);
|
setState(() => _isLoadingLocation = true);
|
||||||
// START CHANGE: Use the correct, new service type from Provider
|
|
||||||
final service = Provider.of<MarineInSituSamplingService>(context, listen: false);
|
final service = Provider.of<MarineInSituSamplingService>(context, listen: false);
|
||||||
// END CHANGE
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final position = await service.getCurrentLocation();
|
final position = await service.getCurrentLocation();
|
||||||
@ -154,9 +155,7 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
final lon2Str = widget.data.currentLongitude;
|
final lon2Str = widget.data.currentLongitude;
|
||||||
|
|
||||||
if (lat1Str != null && lon1Str != null && lat2Str != null && lon2Str != null) {
|
if (lat1Str != null && lon1Str != null && lat2Str != null && lon2Str != null) {
|
||||||
// START CHANGE: Use the correct, new service type from Provider
|
|
||||||
final service = Provider.of<MarineInSituSamplingService>(context, listen: false);
|
final service = Provider.of<MarineInSituSamplingService>(context, listen: false);
|
||||||
// END CHANGE
|
|
||||||
final lat1 = double.tryParse(lat1Str);
|
final lat1 = double.tryParse(lat1Str);
|
||||||
final lon1 = double.tryParse(lon1Str);
|
final lon1 = double.tryParse(lon1Str);
|
||||||
final lat2 = double.tryParse(lat2Str);
|
final lat2 = double.tryParse(lat2Str);
|
||||||
@ -184,31 +183,113 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates the form and distance, then proceeds to the next step.
|
// --- START: New function to find and show nearby stations ---
|
||||||
|
Future<void> _findAndShowNearbyStations() async {
|
||||||
|
if (widget.data.currentLatitude == null || widget.data.currentLatitude!.isEmpty) {
|
||||||
|
await _getCurrentLocation();
|
||||||
|
if (!mounted || widget.data.currentLatitude == null || widget.data.currentLatitude!.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final service = Provider.of<MarineInSituSamplingService>(context, listen: false);
|
||||||
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
|
||||||
|
final currentLat = double.parse(widget.data.currentLatitude!);
|
||||||
|
final currentLon = double.parse(widget.data.currentLongitude!);
|
||||||
|
final allStations = auth.manualStations ?? [];
|
||||||
|
final List<Map<String, dynamic>> nearbyStations = [];
|
||||||
|
|
||||||
|
for (var station in allStations) {
|
||||||
|
final stationLat = station['man_latitude'];
|
||||||
|
final stationLon = station['man_longitude'];
|
||||||
|
|
||||||
|
if (stationLat is num && stationLon is num) {
|
||||||
|
final distance = service.calculateDistance(currentLat, currentLon, stationLat.toDouble(), stationLon.toDouble());
|
||||||
|
if (distance <= 5.0) { // 5km radius
|
||||||
|
nearbyStations.add({'station': station, 'distance': distance});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nearbyStations.sort((a, b) => a['distance'].compareTo(b['distance']));
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
final selectedStation = await showDialog<Map<String, dynamic>>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _NearbyStationsDialog(nearbyStations: nearbyStations),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedStation != null) {
|
||||||
|
_updateFormWithSelectedStation(selectedStation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END: New function ---
|
||||||
|
|
||||||
|
// --- START: New helper to update form after selection ---
|
||||||
|
void _updateFormWithSelectedStation(Map<String, dynamic> station) {
|
||||||
|
final allStations = Provider.of<AuthProvider>(context, listen: false).manualStations ?? [];
|
||||||
|
setState(() {
|
||||||
|
// Update State
|
||||||
|
widget.data.selectedStateName = station['state_name'];
|
||||||
|
|
||||||
|
// Update Category List based on new State
|
||||||
|
final categories = allStations
|
||||||
|
.where((s) => s['state_name'] == widget.data.selectedStateName)
|
||||||
|
.map((s) => s['category_name'] as String?)
|
||||||
|
.whereType<String>()
|
||||||
|
.toSet()
|
||||||
|
.toList();
|
||||||
|
categories.sort();
|
||||||
|
_categoriesForState = categories;
|
||||||
|
|
||||||
|
// Update Category
|
||||||
|
widget.data.selectedCategoryName = station['category_name'];
|
||||||
|
|
||||||
|
// Update Station List based on new State and Category
|
||||||
|
_stationsForCategory = allStations
|
||||||
|
.where((s) =>
|
||||||
|
s['state_name'] == widget.data.selectedStateName &&
|
||||||
|
s['category_name'] == widget.data.selectedCategoryName)
|
||||||
|
.toList()
|
||||||
|
..sort((a, b) => (a['man_station_code'] ?? '').compareTo(b['man_station_code'] ?? ''));
|
||||||
|
|
||||||
|
// Update Selected Station and its coordinates
|
||||||
|
widget.data.selectedStation = station;
|
||||||
|
widget.data.stationLatitude = station['man_latitude']?.toString();
|
||||||
|
widget.data.stationLongitude = station['man_longitude']?.toString();
|
||||||
|
_stationLatController.text = widget.data.stationLatitude ?? '';
|
||||||
|
_stationLonController.text = widget.data.stationLongitude ?? '';
|
||||||
|
|
||||||
|
// Recalculate distance
|
||||||
|
_calculateDistance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// --- END: New helper ---
|
||||||
|
|
||||||
void _goToNextStep() {
|
void _goToNextStep() {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
_formKey.currentState!.save();
|
_formKey.currentState!.save();
|
||||||
|
|
||||||
final distanceInMeters = (widget.data.distanceDifferenceInKm ?? 0) * 1000;
|
final distanceInMeters = (widget.data.distanceDifferenceInKm ?? 0) * 1000;
|
||||||
|
|
||||||
if (distanceInMeters > 700) {
|
if (distanceInMeters > 50) {
|
||||||
_showDistanceRemarkDialog();
|
_showDistanceRemarkDialog();
|
||||||
} else {
|
} else {
|
||||||
// If distance is okay, clear any previous remarks and proceed.
|
|
||||||
widget.data.distanceDifferenceRemarks = null;
|
widget.data.distanceDifferenceRemarks = null;
|
||||||
widget.onNext();
|
widget.onNext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a dialog to force the user to enter remarks for large distance differences.
|
|
||||||
Future<void> _showDistanceRemarkDialog() async {
|
Future<void> _showDistanceRemarkDialog() async {
|
||||||
final remarkController = TextEditingController(text: widget.data.distanceDifferenceRemarks);
|
final remarkController = TextEditingController(text: widget.data.distanceDifferenceRemarks);
|
||||||
final dialogFormKey = GlobalKey<FormState>();
|
final dialogFormKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
return showDialog<void>(
|
return showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false, // User must interact with the dialog
|
barrierDismissible: false,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Distance Warning'),
|
title: const Text('Distance Warning'),
|
||||||
@ -219,7 +300,7 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text('Your current location is more than 700m away from the station.'),
|
const Text('Your current location is more than 50m away from the station.'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: remarkController,
|
controller: remarkController,
|
||||||
@ -255,7 +336,7 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
widget.data.distanceDifferenceRemarks = remarkController.text;
|
widget.data.distanceDifferenceRemarks = remarkController.text;
|
||||||
});
|
});
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
widget.onNext(); // Proceed to next step
|
widget.onNext();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -270,14 +351,14 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
final allStations = auth.manualStations ?? [];
|
final allStations = auth.manualStations ?? [];
|
||||||
final allUsers = auth.allUsers ?? [];
|
final allUsers = auth.allUsers ?? [];
|
||||||
final secondSamplersList = allUsers.where((user) => user['user_id'] != auth.profileData?['user_id']).toList();
|
final secondSamplersList = allUsers.where((user) => user['user_id'] != auth.profileData?['user_id']).toList()
|
||||||
|
..sort((a, b) => (a['first_name'] ?? '').compareTo(b['first_name'] ?? ''));
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
children: [
|
children: [
|
||||||
// Sampling Information section...
|
|
||||||
Text("Sampling Information", style: Theme.of(context).textTheme.headlineSmall),
|
Text("Sampling Information", style: Theme.of(context).textTheme.headlineSmall),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
TextFormField(controller: _firstSamplerController, readOnly: true, decoration: const InputDecoration(labelText: '1st Sampler')),
|
TextFormField(controller: _firstSamplerController, readOnly: true, decoration: const InputDecoration(labelText: '1st Sampler')),
|
||||||
@ -308,7 +389,6 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Station Selection section...
|
|
||||||
Text("Station Selection", style: Theme.of(context).textTheme.titleLarge),
|
Text("Station Selection", style: Theme.of(context).textTheme.titleLarge),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
DropdownSearch<String>(
|
DropdownSearch<String>(
|
||||||
@ -355,7 +435,12 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
_stationLatController.clear();
|
_stationLatController.clear();
|
||||||
_stationLonController.clear();
|
_stationLonController.clear();
|
||||||
widget.data.distanceDifferenceInKm = null;
|
widget.data.distanceDifferenceInKm = null;
|
||||||
_stationsForCategory = category != null ? allStations.where((s) => s['state_name'] == widget.data.selectedStateName && s['category_name'] == category).toList() : [];
|
_stationsForCategory = category != null
|
||||||
|
? (allStations
|
||||||
|
.where((s) => s['state_name'] == widget.data.selectedStateName && s['category_name'] == category)
|
||||||
|
.toList()
|
||||||
|
..sort((a, b) => (a['man_station_code'] ?? '').compareTo(b['man_station_code'] ?? '')))
|
||||||
|
: [];
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
validator: (val) => widget.data.selectedStateName != null && val == null ? "Category is required" : null,
|
validator: (val) => widget.data.selectedStateName != null && val == null ? "Category is required" : null,
|
||||||
@ -382,9 +467,20 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
TextFormField(controller: _stationLatController, readOnly: true, decoration: const InputDecoration(labelText: 'Station Latitude')),
|
TextFormField(controller: _stationLatController, readOnly: true, decoration: const InputDecoration(labelText: 'Station Latitude')),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(controller: _stationLonController, readOnly: true, decoration: const InputDecoration(labelText: 'Station Longitude')),
|
TextFormField(controller: _stationLonController, readOnly: true, decoration: const InputDecoration(labelText: 'Station Longitude')),
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Location Verification section...
|
// --- START: Added Nearby Station Button ---
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.explore_outlined),
|
||||||
|
label: const Text("NEARBY STATION"),
|
||||||
|
onPressed: _isLoadingLocation ? null : _findAndShowNearbyStations,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// --- END: Added Nearby Station Button ---
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
Text("Location Verification", style: Theme.of(context).textTheme.titleLarge),
|
Text("Location Verification", style: Theme.of(context).textTheme.titleLarge),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(controller: _currentLatController, readOnly: true, decoration: const InputDecoration(labelText: 'Current Latitude')),
|
TextFormField(controller: _currentLatController, readOnly: true, decoration: const InputDecoration(labelText: 'Current Latitude')),
|
||||||
@ -396,9 +492,9 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ((widget.data.distanceDifferenceInKm ?? 0) * 1000) > 700 ? Colors.red.withOpacity(0.1) : Colors.green.withOpacity(0.1),
|
color: ((widget.data.distanceDifferenceInKm ?? 0) * 1000) > 50 ? Colors.red.withOpacity(0.1) : Colors.green.withOpacity(0.1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: ((widget.data.distanceDifferenceInKm ?? 0) * 1000) > 700 ? Colors.red : Colors.green),
|
border: Border.all(color: ((widget.data.distanceDifferenceInKm ?? 0) * 1000) > 50 ? Colors.red : Colors.green),
|
||||||
),
|
),
|
||||||
child: RichText(
|
child: RichText(
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@ -410,7 +506,7 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
text: '${(widget.data.distanceDifferenceInKm! * 1000).toStringAsFixed(0)} meters',
|
text: '${(widget.data.distanceDifferenceInKm! * 1000).toStringAsFixed(0)} meters',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: ((widget.data.distanceDifferenceInKm ?? 0) * 1000) > 700 ? Colors.red : Colors.green
|
color: ((widget.data.distanceDifferenceInKm ?? 0) * 1000) > 50 ? Colors.red : Colors.green
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -449,4 +545,50 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: New Dialog Widget for Nearby Stations ---
|
||||||
|
class _NearbyStationsDialog extends StatelessWidget {
|
||||||
|
final List<Map<String, dynamic>> nearbyStations;
|
||||||
|
|
||||||
|
const _NearbyStationsDialog({required this.nearbyStations});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Nearby Stations (within 5km)'),
|
||||||
|
content: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: nearbyStations.isEmpty
|
||||||
|
? const Center(child: Text('No stations found.'))
|
||||||
|
: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: nearbyStations.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = nearbyStations[index];
|
||||||
|
final station = item['station'] as Map<String, dynamic>;
|
||||||
|
final distanceInMeters = (item['distance'] as double) * 1000;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text("${station['man_station_code'] ?? 'N/A'}"),
|
||||||
|
subtitle: Text("${station['man_station_name'] ?? 'N/A'}"),
|
||||||
|
trailing: Text("${distanceInMeters.toStringAsFixed(0)} m"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop(station);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END: New Dialog Widget ---
|
||||||
@ -6,9 +6,7 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../../../models/in_situ_sampling_data.dart';
|
import '../../../../models/in_situ_sampling_data.dart';
|
||||||
// START CHANGE: Import the new, correct service file
|
|
||||||
import '../../../../services/marine_in_situ_sampling_service.dart';
|
import '../../../../services/marine_in_situ_sampling_service.dart';
|
||||||
// END CHANGE
|
|
||||||
|
|
||||||
/// The second step of the In-Situ Sampling form.
|
/// The second step of the In-Situ Sampling form.
|
||||||
/// Gathers on-site conditions (weather, tide) and handles all photo attachments.
|
/// Gathers on-site conditions (weather, tide) and handles all photo attachments.
|
||||||
@ -30,13 +28,10 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
bool _isPickingImage = false;
|
bool _isPickingImage = false;
|
||||||
|
|
||||||
// --- UI Controllers for remarks ---
|
// --- START MODIFICATION: Removed optional remark controllers ---
|
||||||
late final TextEditingController _eventRemarksController;
|
late final TextEditingController _eventRemarksController;
|
||||||
late final TextEditingController _labRemarksController;
|
late final TextEditingController _labRemarksController;
|
||||||
late final TextEditingController _optionalRemark1Controller;
|
// --- END MODIFICATION ---
|
||||||
late final TextEditingController _optionalRemark2Controller;
|
|
||||||
late final TextEditingController _optionalRemark3Controller;
|
|
||||||
late final TextEditingController _optionalRemark4Controller;
|
|
||||||
|
|
||||||
|
|
||||||
final List<String> _weatherOptions = ['Clear', 'Rainy', 'Cloudy'];
|
final List<String> _weatherOptions = ['Clear', 'Rainy', 'Cloudy'];
|
||||||
@ -48,20 +43,16 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_eventRemarksController = TextEditingController(text: widget.data.eventRemarks);
|
_eventRemarksController = TextEditingController(text: widget.data.eventRemarks);
|
||||||
_labRemarksController = TextEditingController(text: widget.data.labRemarks);
|
_labRemarksController = TextEditingController(text: widget.data.labRemarks);
|
||||||
_optionalRemark1Controller = TextEditingController(text: widget.data.optionalRemark1);
|
// --- START MODIFICATION: Removed initialization for optional remark controllers ---
|
||||||
_optionalRemark2Controller = TextEditingController(text: widget.data.optionalRemark2);
|
// --- END MODIFICATION ---
|
||||||
_optionalRemark3Controller = TextEditingController(text: widget.data.optionalRemark3);
|
|
||||||
_optionalRemark4Controller = TextEditingController(text: widget.data.optionalRemark4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_eventRemarksController.dispose();
|
_eventRemarksController.dispose();
|
||||||
_labRemarksController.dispose();
|
_labRemarksController.dispose();
|
||||||
_optionalRemark1Controller.dispose();
|
// --- START MODIFICATION: Removed disposal of optional remark controllers ---
|
||||||
_optionalRemark2Controller.dispose();
|
// --- END MODIFICATION ---
|
||||||
_optionalRemark3Controller.dispose();
|
|
||||||
_optionalRemark4Controller.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,9 +61,7 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
|||||||
if (_isPickingImage) return;
|
if (_isPickingImage) return;
|
||||||
setState(() => _isPickingImage = true);
|
setState(() => _isPickingImage = true);
|
||||||
|
|
||||||
// START CHANGE: Use the correct service type from Provider
|
|
||||||
final service = Provider.of<MarineInSituSamplingService>(context, listen: false);
|
final service = Provider.of<MarineInSituSamplingService>(context, listen: false);
|
||||||
// END CHANGE
|
|
||||||
|
|
||||||
final file = await service.pickAndProcessImage(source, data: widget.data, imageInfo: imageInfo, isRequired: isRequired);
|
final file = await service.pickAndProcessImage(source, data: widget.data, imageInfo: imageInfo, isRequired: isRequired);
|
||||||
|
|
||||||
@ -89,26 +78,25 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
|||||||
|
|
||||||
/// Validates the form and all required images before proceeding.
|
/// Validates the form and all required images before proceeding.
|
||||||
void _goToNextStep() {
|
void _goToNextStep() {
|
||||||
|
// --- START MODIFICATION: Updated validation logic ---
|
||||||
|
if (widget.data.leftLandViewImage == null ||
|
||||||
|
widget.data.rightLandViewImage == null ||
|
||||||
|
widget.data.waterFillingImage == null ||
|
||||||
|
widget.data.seawaterColorImage == null) {
|
||||||
|
_showSnackBar('Please attach all 4 required photos before proceeding.', isError: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form validation now handles the conditional requirement for Event Remarks
|
||||||
if (!_formKey.currentState!.validate()) {
|
if (!_formKey.currentState!.validate()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.data.leftLandViewImage == null ||
|
|
||||||
widget.data.rightLandViewImage == null ||
|
|
||||||
widget.data.waterFillingImage == null ||
|
|
||||||
widget.data.seawaterColorImage == null ||
|
|
||||||
widget.data.phPaperImage == null) {
|
|
||||||
_showSnackBar('Please attach all 5 required photos before proceeding.', isError: true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_formKey.currentState!.save();
|
_formKey.currentState!.save();
|
||||||
// --- FIXED: Correctly save remarks text to the data model's remark properties ---
|
|
||||||
widget.data.optionalRemark1 = _optionalRemark1Controller.text;
|
// Removed saving of optional remarks as they are no longer present
|
||||||
widget.data.optionalRemark2 = _optionalRemark2Controller.text;
|
|
||||||
widget.data.optionalRemark3 = _optionalRemark3Controller.text;
|
|
||||||
widget.data.optionalRemark4 = _optionalRemark4Controller.text;
|
|
||||||
widget.onNext();
|
widget.onNext();
|
||||||
|
// --- END MODIFICATION ---
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showSnackBar(String message, {bool isError = false}) {
|
void _showSnackBar(String message, {bool isError = false}) {
|
||||||
@ -122,6 +110,14 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// --- START MODIFICATION: Logic to determine if Event Remarks are required ---
|
||||||
|
final bool areAdditionalPhotosAttached = widget.data.phPaperImage != null ||
|
||||||
|
widget.data.optionalImage1 != null ||
|
||||||
|
widget.data.optionalImage2 != null ||
|
||||||
|
widget.data.optionalImage3 != null ||
|
||||||
|
widget.data.optionalImage4 != null;
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
@ -163,27 +159,39 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
|||||||
_buildImagePicker('Right Side Land View', 'RIGHT_LAND_VIEW', widget.data.rightLandViewImage, (file) => widget.data.rightLandViewImage = file, isRequired: true),
|
_buildImagePicker('Right Side Land View', 'RIGHT_LAND_VIEW', widget.data.rightLandViewImage, (file) => widget.data.rightLandViewImage = file, isRequired: true),
|
||||||
_buildImagePicker('Filling Water into Sample Bottle', 'WATER_FILLING', widget.data.waterFillingImage, (file) => widget.data.waterFillingImage = file, isRequired: true),
|
_buildImagePicker('Filling Water into Sample Bottle', 'WATER_FILLING', widget.data.waterFillingImage, (file) => widget.data.waterFillingImage = file, isRequired: true),
|
||||||
_buildImagePicker('Seawater in Clear Glass Bottle', 'SEAWATER_COLOR', widget.data.seawaterColorImage, (file) => widget.data.seawaterColorImage = file, isRequired: true),
|
_buildImagePicker('Seawater in Clear Glass Bottle', 'SEAWATER_COLOR', widget.data.seawaterColorImage, (file) => widget.data.seawaterColorImage = file, isRequired: true),
|
||||||
_buildImagePicker('Examine Preservative (pH paper)', 'PH_PAPER', widget.data.phPaperImage, (file) => widget.data.phPaperImage = file, isRequired: true),
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// --- Section: Optional Photos ---
|
// --- START MODIFICATION: Section for additional photos and conditional remarks ---
|
||||||
Text("Optional Photos & Remarks", style: Theme.of(context).textTheme.titleLarge),
|
Text("Additional Photos & Remarks", style: Theme.of(context).textTheme.titleLarge),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildImagePicker('Optional Photo 1', 'OPTIONAL_1', widget.data.optionalImage1, (file) => widget.data.optionalImage1 = file, remarkController: _optionalRemark1Controller, isRequired: false),
|
// pH Paper photo is now the first optional photo
|
||||||
_buildImagePicker('Optional Photo 2', 'OPTIONAL_2', widget.data.optionalImage2, (file) => widget.data.optionalImage2 = file, remarkController: _optionalRemark2Controller, isRequired: false),
|
_buildImagePicker('Examine Preservative (pH paper)', 'PH_PAPER', widget.data.phPaperImage, (file) => widget.data.phPaperImage = file, isRequired: false),
|
||||||
_buildImagePicker('Optional Photo 3', 'OPTIONAL_3', widget.data.optionalImage3, (file) => widget.data.optionalImage3 = file, remarkController: _optionalRemark3Controller, isRequired: false),
|
// Other optional photos no longer have remark fields
|
||||||
_buildImagePicker('Optional Photo 4', 'OPTIONAL_4', widget.data.optionalImage4, (file) => widget.data.optionalImage4 = file, remarkController: _optionalRemark4Controller, isRequired: false),
|
_buildImagePicker('Optional Photo 1', 'OPTIONAL_1', widget.data.optionalImage1, (file) => widget.data.optionalImage1 = file, isRequired: false),
|
||||||
|
_buildImagePicker('Optional Photo 2', 'OPTIONAL_2', widget.data.optionalImage2, (file) => widget.data.optionalImage2 = file, isRequired: false),
|
||||||
|
_buildImagePicker('Optional Photo 3', 'OPTIONAL_3', widget.data.optionalImage3, (file) => widget.data.optionalImage3 = file, isRequired: false),
|
||||||
|
_buildImagePicker('Optional Photo 4', 'OPTIONAL_4', widget.data.optionalImage4, (file) => widget.data.optionalImage4 = file, isRequired: false),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// --- Section: Remarks ---
|
|
||||||
Text("Remarks", style: Theme.of(context).textTheme.titleLarge),
|
Text("Remarks", style: Theme.of(context).textTheme.titleLarge),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
// Event Remarks field is now conditionally required
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _eventRemarksController,
|
controller: _eventRemarksController,
|
||||||
decoration: const InputDecoration(labelText: 'Event Remarks (Optional)', hintText: 'e.g., unusual smells, colors, etc.'),
|
decoration: InputDecoration(
|
||||||
|
labelText: areAdditionalPhotosAttached ? 'Event Remarks *' : 'Event Remarks (Optional)',
|
||||||
|
hintText: 'e.g., unusual smells, colors, etc.'
|
||||||
|
),
|
||||||
onSaved: (value) => widget.data.eventRemarks = value,
|
onSaved: (value) => widget.data.eventRemarks = value,
|
||||||
|
validator: (value) {
|
||||||
|
if (areAdditionalPhotosAttached && (value == null || value.trim().isEmpty)) {
|
||||||
|
return 'Event Remarks are required when attaching additional photos.';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
),
|
),
|
||||||
|
// --- END MODIFICATION ---
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _labRemarksController,
|
controller: _labRemarksController,
|
||||||
@ -203,7 +211,9 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A reusable widget for picking and displaying an image, matching the tarball design.
|
/// A reusable widget for picking and displaying an image, matching the tarball design.
|
||||||
Widget _buildImagePicker(String title, String imageInfo, File? imageFile, Function(File?) setImageCallback, {TextEditingController? remarkController, bool isRequired = false}) {
|
// --- START MODIFICATION: Removed remarkController parameter ---
|
||||||
|
Widget _buildImagePicker(String title, String imageInfo, File? imageFile, Function(File?) setImageCallback, {bool isRequired = false}) {
|
||||||
|
// --- END MODIFICATION ---
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -235,18 +245,8 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
|||||||
ElevatedButton.icon(onPressed: _isPickingImage ? null : () => _setImage(setImageCallback, ImageSource.gallery, imageInfo, isRequired: isRequired), icon: const Icon(Icons.photo_library), label: const Text("Gallery")),
|
ElevatedButton.icon(onPressed: _isPickingImage ? null : () => _setImage(setImageCallback, ImageSource.gallery, imageInfo, isRequired: isRequired), icon: const Icon(Icons.photo_library), label: const Text("Gallery")),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (remarkController != null)
|
// --- START MODIFICATION: Removed remark text field ---
|
||||||
Padding(
|
// --- END MODIFICATION ---
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: TextFormField(
|
|
||||||
controller: remarkController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Remarks for $title',
|
|
||||||
hintText: 'Add an optional remark...',
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
// lib/screens/register.dart
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -33,6 +35,20 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
int? _selectedCompanyId;
|
int? _selectedCompanyId;
|
||||||
int? _selectedPositionId;
|
int? _selectedPositionId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Use addPostFrameCallback to safely access the Provider after the first frame.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
// Check if data is already loaded to avoid unnecessary API calls.
|
||||||
|
if (auth.departments == null || auth.departments!.isEmpty) {
|
||||||
|
// Call the new, specific function that works without a login token.
|
||||||
|
auth.syncRegistrationData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_usernameController.dispose();
|
_usernameController.dispose();
|
||||||
@ -120,6 +136,24 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
final companies = auth.companies ?? [];
|
final companies = auth.companies ?? [];
|
||||||
final positions = auth.positions ?? [];
|
final positions = auth.positions ?? [];
|
||||||
|
|
||||||
|
// If the lists are empty, it means data is likely still loading.
|
||||||
|
// Show a loading indicator.
|
||||||
|
if (departments.isEmpty && companies.isEmpty && positions.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(32.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text("Loading registration options..."),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@ -36,12 +36,10 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
late final TextEditingController _stationLonController;
|
late final TextEditingController _stationLonController;
|
||||||
late final TextEditingController _currentLatController;
|
late final TextEditingController _currentLatController;
|
||||||
late final TextEditingController _currentLonController;
|
late final TextEditingController _currentLonController;
|
||||||
// REMOVED: Controllers for weather and remarks.
|
|
||||||
|
|
||||||
List<String> _statesList = [];
|
List<String> _statesList = [];
|
||||||
List<Map<String, dynamic>> _stationsForState = [];
|
List<Map<String, dynamic>> _stationsForState = [];
|
||||||
final List<String> _samplingTypes = ['Schedule', 'Triennial'];
|
final List<String> _samplingTypes = ['Schedule', 'Triennial'];
|
||||||
// REMOVED: Weather options list.
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -60,7 +58,6 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
_stationLonController.dispose();
|
_stationLonController.dispose();
|
||||||
_currentLatController.dispose();
|
_currentLatController.dispose();
|
||||||
_currentLonController.dispose();
|
_currentLonController.dispose();
|
||||||
// REMOVED: Dispose controllers for remarks.
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +70,6 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
_stationLonController = TextEditingController(text: widget.data.stationLongitude);
|
_stationLonController = TextEditingController(text: widget.data.stationLongitude);
|
||||||
_currentLatController = TextEditingController(text: widget.data.currentLatitude);
|
_currentLatController = TextEditingController(text: widget.data.currentLatitude);
|
||||||
_currentLonController = TextEditingController(text: widget.data.currentLongitude);
|
_currentLonController = TextEditingController(text: widget.data.currentLongitude);
|
||||||
// REMOVED: Initialize controllers for remarks.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initializeForm() {
|
void _initializeForm() {
|
||||||
@ -91,6 +87,10 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
_dateController.text = widget.data.samplingDate!;
|
_dateController.text = widget.data.samplingDate!;
|
||||||
_timeController.text = widget.data.samplingTime!;
|
_timeController.text = widget.data.samplingTime!;
|
||||||
|
|
||||||
|
if (widget.data.samplingType == null) {
|
||||||
|
widget.data.samplingType = 'Schedule';
|
||||||
|
}
|
||||||
|
|
||||||
final allStations = auth.riverManualStations ?? [];
|
final allStations = auth.riverManualStations ?? [];
|
||||||
if (allStations.isNotEmpty) {
|
if (allStations.isNotEmpty) {
|
||||||
final states = allStations.map((s) => s['state_name'] as String?).whereType<String>().toSet().toList();
|
final states = allStations.map((s) => s['state_name'] as String?).whereType<String>().toSet().toList();
|
||||||
@ -99,7 +99,10 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
if (widget.data.selectedStateName != null) {
|
if (widget.data.selectedStateName != null) {
|
||||||
_stationsForState = allStations
|
_stationsForState = allStations
|
||||||
.where((s) => s['state_name'] == widget.data.selectedStateName)
|
.where((s) => s['state_name'] == widget.data.selectedStateName)
|
||||||
.toList();
|
.toList()
|
||||||
|
// --- START MODIFICATION: Sort stations on initial load ---
|
||||||
|
..sort((a, b) => (a['sampling_station_code'] ?? '').compareTo(b['sampling_station_code'] ?? ''));
|
||||||
|
// --- END MODIFICATION ---
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -169,13 +172,75 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _findAndShowNearbyStations() async {
|
||||||
|
if (widget.data.currentLatitude == null || widget.data.currentLatitude!.isEmpty) {
|
||||||
|
await _getCurrentLocation();
|
||||||
|
if (!mounted || widget.data.currentLatitude == null || widget.data.currentLatitude!.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final service = Provider.of<RiverInSituSamplingService>(context, listen: false);
|
||||||
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
|
||||||
|
final currentLat = double.parse(widget.data.currentLatitude!);
|
||||||
|
final currentLon = double.parse(widget.data.currentLongitude!);
|
||||||
|
final allStations = auth.riverManualStations ?? [];
|
||||||
|
final List<Map<String, dynamic>> nearbyStations = [];
|
||||||
|
|
||||||
|
for (var station in allStations) {
|
||||||
|
final stationLat = station['sampling_lat'];
|
||||||
|
final stationLon = station['sampling_long'];
|
||||||
|
|
||||||
|
if (stationLat is num && stationLon is num) {
|
||||||
|
final distance = service.calculateDistance(currentLat, currentLon, stationLat.toDouble(), stationLon.toDouble());
|
||||||
|
if (distance <= 3.0) {
|
||||||
|
nearbyStations.add({'station': station, 'distance': distance});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nearbyStations.sort((a, b) => a['distance'].compareTo(b['distance']));
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
final selectedStation = await showDialog<Map<String, dynamic>>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _NearbyStationsDialog(nearbyStations: nearbyStations),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedStation != null) {
|
||||||
|
_updateFormWithSelectedStation(selectedStation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateFormWithSelectedStation(Map<String, dynamic> station) {
|
||||||
|
final allStations = Provider.of<AuthProvider>(context, listen: false).riverManualStations ?? [];
|
||||||
|
setState(() {
|
||||||
|
widget.data.selectedStateName = station['state_name'];
|
||||||
|
_stationsForState = allStations
|
||||||
|
.where((s) => s['state_name'] == widget.data.selectedStateName)
|
||||||
|
.toList()
|
||||||
|
..sort((a, b) => (a['sampling_station_code'] ?? '').compareTo(b['sampling_station_code'] ?? ''));
|
||||||
|
|
||||||
|
widget.data.selectedStation = station;
|
||||||
|
widget.data.stationLatitude = station['sampling_lat']?.toString();
|
||||||
|
widget.data.stationLongitude = station['sampling_long']?.toString();
|
||||||
|
_stationLatController.text = widget.data.stationLatitude ?? '';
|
||||||
|
_stationLonController.text = widget.data.stationLongitude ?? '';
|
||||||
|
|
||||||
|
_calculateDistance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void _goToNextStep() {
|
void _goToNextStep() {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
_formKey.currentState!.save();
|
_formKey.currentState!.save();
|
||||||
|
|
||||||
final distanceInMeters = (widget.data.distanceDifferenceInKm ?? 0) * 1000;
|
final distanceInMeters = (widget.data.distanceDifferenceInKm ?? 0) * 1000;
|
||||||
|
|
||||||
if (distanceInMeters > 700) {
|
if (distanceInMeters > 50) {
|
||||||
_showDistanceRemarkDialog();
|
_showDistanceRemarkDialog();
|
||||||
} else {
|
} else {
|
||||||
widget.data.distanceDifferenceRemarks = null;
|
widget.data.distanceDifferenceRemarks = null;
|
||||||
@ -201,7 +266,7 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text('Your current location is more than 700m away from the station.'),
|
const Text('Your current location is more than 50m away from the station.'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: remarkController,
|
controller: remarkController,
|
||||||
@ -252,7 +317,11 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
final allStations = auth.riverManualStations ?? [];
|
final allStations = auth.riverManualStations ?? [];
|
||||||
final allUsers = auth.allUsers ?? [];
|
final allUsers = auth.allUsers ?? [];
|
||||||
final secondSamplersList = allUsers.where((user) => user['user_id'] != auth.profileData?['user_id']).toList();
|
|
||||||
|
// --- START MODIFICATION: Sort 2nd sampler list alphabetically ---
|
||||||
|
final secondSamplersList = allUsers.where((user) => user['user_id'] != auth.profileData?['user_id']).toList()
|
||||||
|
..sort((a, b) => (a['first_name'] ?? '').compareTo(b['first_name'] ?? ''));
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@ -318,9 +387,12 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
widget.data.distanceDifferenceInKm = null;
|
widget.data.distanceDifferenceInKm = null;
|
||||||
|
|
||||||
_stationsForState = state != null
|
_stationsForState = state != null
|
||||||
? allStations
|
? (allStations
|
||||||
.where((s) => s['state_name'] == state)
|
.where((s) => s['state_name'] == state)
|
||||||
.toList()
|
.toList()
|
||||||
|
// --- START MODIFICATION: Sort stations when state changes ---
|
||||||
|
..sort((a, b) => (a['sampling_station_code'] ?? '').compareTo(b['sampling_station_code'] ?? '')))
|
||||||
|
// --- END MODIFICATION ---
|
||||||
: [];
|
: [];
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -355,6 +427,17 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
TextFormField(controller: _stationLatController, readOnly: true, decoration: const InputDecoration(labelText: 'Station Latitude')),
|
TextFormField(controller: _stationLatController, readOnly: true, decoration: const InputDecoration(labelText: 'Station Latitude')),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(controller: _stationLonController, readOnly: true, decoration: const InputDecoration(labelText: 'Station Longitude')),
|
TextFormField(controller: _stationLonController, readOnly: true, decoration: const InputDecoration(labelText: 'Station Longitude')),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.explore_outlined),
|
||||||
|
label: const Text("NEARBY STATION"),
|
||||||
|
onPressed: _isLoadingLocation ? null : _findAndShowNearbyStations,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
Text("Location Verification", style: Theme.of(context).textTheme.titleLarge),
|
Text("Location Verification", style: Theme.of(context).textTheme.titleLarge),
|
||||||
@ -368,9 +451,9 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ((widget.data.distanceDifferenceInKm ?? 0) * 1000) > 700 ? Colors.red.withOpacity(0.1) : Colors.green.withOpacity(0.1),
|
color: ((widget.data.distanceDifferenceInKm ?? 0) * 1000) > 50 ? Colors.red.withOpacity(0.1) : Colors.green.withOpacity(0.1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: ((widget.data.distanceDifferenceInKm ?? 0) * 1000) > 700 ? Colors.red : Colors.green),
|
border: Border.all(color: ((widget.data.distanceDifferenceInKm ?? 0) * 1000) > 50 ? Colors.red : Colors.green),
|
||||||
),
|
),
|
||||||
child: RichText(
|
child: RichText(
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@ -382,7 +465,7 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
text: '${(widget.data.distanceDifferenceInKm! * 1000).toStringAsFixed(0)} meters',
|
text: '${(widget.data.distanceDifferenceInKm! * 1000).toStringAsFixed(0)} meters',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: ((widget.data.distanceDifferenceInKm ?? 0) * 1000) > 700 ? Colors.red : Colors.green
|
color: ((widget.data.distanceDifferenceInKm ?? 0) * 1000) > 50 ? Colors.red : Colors.green
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -398,8 +481,6 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// REMOVED: On-Site Information section (Weather, Remarks).
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _goToNextStep,
|
onPressed: _goToNextStep,
|
||||||
@ -410,4 +491,48 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NearbyStationsDialog extends StatelessWidget {
|
||||||
|
final List<Map<String, dynamic>> nearbyStations;
|
||||||
|
|
||||||
|
const _NearbyStationsDialog({required this.nearbyStations});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Nearby Stations (within 3km)'),
|
||||||
|
content: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: nearbyStations.isEmpty
|
||||||
|
? const Center(child: Text('No stations found.'))
|
||||||
|
: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: nearbyStations.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = nearbyStations[index];
|
||||||
|
final station = item['station'] as Map<String, dynamic>;
|
||||||
|
final distanceInMeters = (item['distance'] as double) * 1000;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text("${station['sampling_station_code'] ?? 'N/A'}"),
|
||||||
|
subtitle: Text("${station['sampling_river'] ?? 'N/A'}"),
|
||||||
|
trailing: Text("${distanceInMeters.toStringAsFixed(0)} m"),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop(station);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -28,7 +28,7 @@ class _RiverInSituStep2SiteInfoState extends State<RiverInSituStep2SiteInfo> {
|
|||||||
|
|
||||||
late final TextEditingController _eventRemarksController;
|
late final TextEditingController _eventRemarksController;
|
||||||
late final TextEditingController _labRemarksController;
|
late final TextEditingController _labRemarksController;
|
||||||
final List<String> _weatherOptions = ['Clear', 'Rainy', 'Cloudy'];
|
final List<String> _weatherOptions = ['Cloudy', 'Drizzle', 'Rainy', 'Sunny', 'Windy'];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|||||||
@ -35,8 +35,13 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
bool _isAutoReading = false;
|
bool _isAutoReading = false;
|
||||||
StreamSubscription? _dataSubscription;
|
StreamSubscription? _dataSubscription;
|
||||||
|
|
||||||
|
// --- START FIX: Declare service variable for safe disposal ---
|
||||||
|
late final RiverInSituSamplingService _samplingService;
|
||||||
|
// --- END FIX ---
|
||||||
|
|
||||||
// --- START: Added for Parameter Validation Feature ---
|
// --- START: Added for Parameter Validation Feature ---
|
||||||
Map<String, double>? _previousReadingsForComparison;
|
Map<String, double>? _previousReadingsForComparison;
|
||||||
|
Set<String> _outOfBoundsKeys = {};
|
||||||
|
|
||||||
final Map<String, String> _parameterKeyToLimitName = const {
|
final Map<String, String> _parameterKeyToLimitName = const {
|
||||||
'oxygenConcentration': 'Oxygen Conc',
|
'oxygenConcentration': 'Oxygen Conc',
|
||||||
@ -48,6 +53,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
'tds': 'TDS',
|
'tds': 'TDS',
|
||||||
'turbidity': 'Turbidity',
|
'turbidity': 'Turbidity',
|
||||||
'ammonia': 'Ammonia',
|
'ammonia': 'Ammonia',
|
||||||
|
'batteryVoltage': 'Battery',
|
||||||
};
|
};
|
||||||
// --- END: Added for Parameter Validation Feature ---
|
// --- END: Added for Parameter Validation Feature ---
|
||||||
|
|
||||||
@ -79,6 +85,9 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
// --- START FIX: Initialize service variable safely ---
|
||||||
|
_samplingService = Provider.of<RiverInSituSamplingService>(context, listen: false);
|
||||||
|
// --- END FIX ---
|
||||||
_initializeControllers();
|
_initializeControllers();
|
||||||
_initializeFlowrateControllers();
|
_initializeFlowrateControllers();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
@ -87,6 +96,16 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_dataSubscription?.cancel();
|
_dataSubscription?.cancel();
|
||||||
|
|
||||||
|
// --- START FIX: Properly disconnect from active connections on dispose ---
|
||||||
|
if (_samplingService.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||||
|
_samplingService.disconnectFromBluetooth();
|
||||||
|
}
|
||||||
|
if (_samplingService.serialConnectionState.value != SerialConnectionState.disconnected) {
|
||||||
|
_samplingService.disconnectFromSerial();
|
||||||
|
}
|
||||||
|
// --- END FIX ---
|
||||||
|
|
||||||
_disposeControllers();
|
_disposeControllers();
|
||||||
_disposeFlowrateControllers();
|
_disposeFlowrateControllers();
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
@ -123,7 +142,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
|
|
||||||
if (_parameters.isEmpty) {
|
if (_parameters.isEmpty) {
|
||||||
_parameters.addAll([
|
_parameters.addAll([
|
||||||
// --- START: Added 'key' for programmatic access ---
|
|
||||||
{'key': 'oxygenConcentration', 'icon': Icons.air, 'label': 'Oxygen Conc.', 'unit': 'mg/L', 'controller': _oxyConcController},
|
{'key': 'oxygenConcentration', 'icon': Icons.air, 'label': 'Oxygen Conc.', 'unit': 'mg/L', 'controller': _oxyConcController},
|
||||||
{'key': 'oxygenSaturation', 'icon': Icons.percent, 'label': 'Oxygen Sat.', 'unit': '%', 'controller': _oxySatController},
|
{'key': 'oxygenSaturation', 'icon': Icons.percent, 'label': 'Oxygen Sat.', 'unit': '%', 'controller': _oxySatController},
|
||||||
{'key': 'ph', 'icon': Icons.science_outlined, 'label': 'pH', 'unit': '', 'controller': _phController},
|
{'key': 'ph', 'icon': Icons.science_outlined, 'label': 'pH', 'unit': '', 'controller': _phController},
|
||||||
@ -134,7 +152,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
{'key': 'turbidity', 'icon': Icons.opacity, 'label': 'Turbidity', 'unit': 'NTU', 'controller': _turbidityController},
|
{'key': 'turbidity', 'icon': Icons.opacity, 'label': 'Turbidity', 'unit': 'NTU', 'controller': _turbidityController},
|
||||||
{'key': 'ammonia', 'icon': Icons.science, 'label': 'Ammonia', 'unit': 'mg/L', 'controller': _ammoniaController},
|
{'key': 'ammonia', 'icon': Icons.science, 'label': 'Ammonia', 'unit': 'mg/L', 'controller': _ammoniaController},
|
||||||
{'key': 'batteryVoltage', 'icon': Icons.battery_charging_full, 'label': 'Battery', 'unit': 'V', 'controller': _batteryController},
|
{'key': 'batteryVoltage', 'icon': Icons.battery_charging_full, 'label': 'Battery', 'unit': 'V', 'controller': _batteryController},
|
||||||
// --- END: Added 'key' for programmatic access ---
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -357,6 +374,10 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
final riverLimits = allLimits.where((limit) => limit['department_id'] == 3).toList();
|
final riverLimits = allLimits.where((limit) => limit['department_id'] == 3).toList();
|
||||||
final outOfBoundsParams = _validateParameters(currentReadings, riverLimits);
|
final outOfBoundsParams = _validateParameters(currentReadings, riverLimits);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_outOfBoundsKeys = outOfBoundsParams.map((p) => _parameters.firstWhere((param) => param['label'] == p['label'])['key'] as String).toSet();
|
||||||
|
});
|
||||||
|
|
||||||
if (outOfBoundsParams.isNotEmpty) {
|
if (outOfBoundsParams.isNotEmpty) {
|
||||||
_showParameterLimitDialog(outOfBoundsParams, currentReadings);
|
_showParameterLimitDialog(outOfBoundsParams, currentReadings);
|
||||||
} else {
|
} else {
|
||||||
@ -445,11 +466,12 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_previousReadingsForComparison != null) {
|
setState(() {
|
||||||
setState(() {
|
_outOfBoundsKeys.clear();
|
||||||
|
if (_previousReadingsForComparison != null) {
|
||||||
_previousReadingsForComparison = null;
|
_previousReadingsForComparison = null;
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
widget.onNext();
|
widget.onNext();
|
||||||
}
|
}
|
||||||
@ -549,11 +571,11 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
Expanded(child: TextFormField(controller: _timeController, readOnly: true, decoration: const InputDecoration(labelText: 'Time'))),
|
Expanded(child: TextFormField(controller: _timeController, readOnly: true, decoration: const InputDecoration(labelText: 'Time'))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(height: 32),
|
|
||||||
|
|
||||||
if (_previousReadingsForComparison != null)
|
if (_previousReadingsForComparison != null)
|
||||||
_buildComparisonView(),
|
_buildComparisonView(),
|
||||||
|
|
||||||
|
const Divider(height: 32),
|
||||||
Column(
|
Column(
|
||||||
children: _parameters.map((param) {
|
children: _parameters.map((param) {
|
||||||
return _buildParameterListItem(
|
return _buildParameterListItem(
|
||||||
@ -561,6 +583,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
label: param['label'] as String,
|
label: param['label'] as String,
|
||||||
unit: param['unit'] as String,
|
unit: param['unit'] as String,
|
||||||
controller: param['controller'] as TextEditingController,
|
controller: param['controller'] as TextEditingController,
|
||||||
|
isOutOfBounds: _outOfBoundsKeys.contains(param['key']),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
@ -577,10 +600,15 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildParameterListItem({ required IconData icon, required String label, required String unit, required TextEditingController controller, }) {
|
Widget _buildParameterListItem({ required IconData icon, required String label, required String unit, required TextEditingController controller, bool isOutOfBounds = false}) {
|
||||||
final bool isMissing = controller.text.isEmpty || controller.text.contains('-999');
|
final bool isMissing = controller.text.isEmpty || controller.text.contains('-999');
|
||||||
final String displayValue = isMissing ? '-.--' : controller.text;
|
final String displayValue = isMissing ? '-.--' : controller.text;
|
||||||
final String displayLabel = unit.isEmpty ? label : '$label ($unit)';
|
final String displayLabel = unit.isEmpty ? label : '$label ($unit)';
|
||||||
|
|
||||||
|
final Color valueColor = isOutOfBounds
|
||||||
|
? Colors.red
|
||||||
|
: (isMissing ? Colors.grey : Theme.of(context).colorScheme.primary);
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
@ -590,7 +618,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
displayValue,
|
displayValue,
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: isMissing ? Colors.grey : Theme.of(context).colorScheme.primary),
|
color: valueColor),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -648,7 +676,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 16.0),
|
margin: const EdgeInsets.only(top: 24.0),
|
||||||
color: Theme.of(context).cardColor,
|
color: Theme.of(context).cardColor,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
@ -688,6 +716,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
final label = param['label'] as String;
|
final label = param['label'] as String;
|
||||||
final controller = param['controller'] as TextEditingController;
|
final controller = param['controller'] as TextEditingController;
|
||||||
final previousValue = previousReadings[key];
|
final previousValue = previousReadings[key];
|
||||||
|
final bool isCurrentValueOutOfBounds = _outOfBoundsKeys.contains(key);
|
||||||
|
|
||||||
return TableRow(
|
return TableRow(
|
||||||
children: [
|
children: [
|
||||||
@ -695,15 +724,20 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
previousValue == -999.0 ? '-.--' : previousValue!.toStringAsFixed(2),
|
previousValue == -999.0 ? '-.--' : previousValue!.toStringAsFixed(5),
|
||||||
style: TextStyle(color: isDarkTheme ? Colors.orange.shade200 : Colors.orange.shade700),
|
style: TextStyle(color: isDarkTheme ? Colors.orange.shade200 : Colors.orange.shade700),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
controller.text.contains('-999') ? '-.--' : (double.tryParse(controller.text) ?? 0).toStringAsFixed(2),
|
controller.text.contains('-999') ? '-.--' : (double.tryParse(controller.text) ?? 0).toStringAsFixed(5),
|
||||||
style: TextStyle(color: isDarkTheme ? Colors.green.shade200 : Colors.green.shade700, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: isCurrentValueOutOfBounds
|
||||||
|
? Colors.red
|
||||||
|
: (isDarkTheme ? Colors.green.shade200 : Colors.green.shade700),
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -759,11 +793,11 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
...invalidParams.map((p) => TableRow(
|
...invalidParams.map((p) => TableRow(
|
||||||
children: [
|
children: [
|
||||||
Padding(padding: const EdgeInsets.all(6.0), child: Text(p['label'])),
|
Padding(padding: const EdgeInsets.all(6.0), child: Text(p['label'])),
|
||||||
Padding(padding: const EdgeInsets.all(6.0), child: Text('${p['lower_limit']?.toStringAsFixed(1) ?? 'N/A'} - ${p['upper_limit']?.toStringAsFixed(1) ?? 'N/A'}')),
|
Padding(padding: const EdgeInsets.all(6.0), child: Text('${p['lower_limit']?.toStringAsFixed(5) ?? 'N/A'} - ${p['upper_limit']?.toStringAsFixed(5) ?? 'N/A'}')),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(6.0),
|
padding: const EdgeInsets.all(6.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
p['value'].toStringAsFixed(2),
|
p['value'].toStringAsFixed(5),
|
||||||
style: const TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold),
|
style: const TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -772,7 +806,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text('Do you want to resample or proceed with the current values?'),
|
const Text('Do you want to resample or proceed with the current values? Please verify with standard solution.'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../../../auth_provider.dart';
|
||||||
import '../../../../models/river_in_situ_sampling_data.dart';
|
import '../../../../models/river_in_situ_sampling_data.dart';
|
||||||
|
|
||||||
class RiverInSituStep5Summary extends StatelessWidget {
|
class RiverInSituStep5Summary extends StatelessWidget {
|
||||||
@ -17,8 +19,73 @@ class RiverInSituStep5Summary extends StatelessWidget {
|
|||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- START: MODIFICATION FOR HIGHLIGHTING ---
|
||||||
|
// Added helper logic to re-validate parameters on the summary screen.
|
||||||
|
|
||||||
|
/// Maps the app's internal parameter keys to the names used in the database.
|
||||||
|
static const Map<String, String> _parameterKeyToLimitName = {
|
||||||
|
'oxygenConcentration': 'Oxygen Conc',
|
||||||
|
'oxygenSaturation': 'Oxygen Sat',
|
||||||
|
'ph': 'pH',
|
||||||
|
'salinity': 'Salinity',
|
||||||
|
'electricalConductivity': 'Conductivity',
|
||||||
|
'temperature': 'Temperature',
|
||||||
|
'tds': 'TDS',
|
||||||
|
'turbidity': 'Turbidity',
|
||||||
|
'ammonia': 'Ammonia',
|
||||||
|
'batteryVoltage': 'Battery',
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Re-validates the final parameters against the defined limits.
|
||||||
|
Set<String> _getOutOfBoundsKeys(BuildContext context) {
|
||||||
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
// Filter for River department (id: 3)
|
||||||
|
final riverLimits = (authProvider.parameterLimits ?? []).where((limit) => limit['department_id'] == 3).toList();
|
||||||
|
final Set<String> invalidKeys = {};
|
||||||
|
|
||||||
|
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,
|
||||||
|
'ammonia': data.ammonia, 'batteryVoltage': data.batteryVoltage,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = riverLimits.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']);
|
||||||
|
|
||||||
|
if ((lowerLimit != null && value < lowerLimit) || (upperLimit != null && value > upperLimit)) {
|
||||||
|
invalidKeys.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return invalidKeys;
|
||||||
|
}
|
||||||
|
// --- END: MODIFICATION FOR HIGHLIGHTING ---
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// --- START: MODIFICATION FOR HIGHLIGHTING ---
|
||||||
|
// Get the set of out-of-bounds keys before building the list.
|
||||||
|
final outOfBoundsKeys = _getOutOfBoundsKeys(context);
|
||||||
|
// --- END: MODIFICATION FOR HIGHLIGHTING ---
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
children: [
|
children: [
|
||||||
@ -92,17 +159,18 @@ class RiverInSituStep5Summary extends StatelessWidget {
|
|||||||
_buildDetailRow("Sonde ID:", data.sondeId),
|
_buildDetailRow("Sonde ID:", data.sondeId),
|
||||||
_buildDetailRow("Capture Time:", "${data.dataCaptureDate} ${data.dataCaptureTime}"),
|
_buildDetailRow("Capture Time:", "${data.dataCaptureDate} ${data.dataCaptureTime}"),
|
||||||
const Divider(height: 20),
|
const Divider(height: 20),
|
||||||
_buildParameterListItem(context, icon: Icons.air, label: "Oxygen Conc.", unit: "mg/L", value: data.oxygenConcentration?.toStringAsFixed(2)),
|
// --- START: MODIFICATION FOR 5 DECIMALS & HIGHLIGHTING ---
|
||||||
_buildParameterListItem(context, icon: Icons.percent, label: "Oxygen Sat.", unit: "%", value: data.oxygenSaturation?.toStringAsFixed(2)),
|
_buildParameterListItem(context, icon: Icons.air, label: "Oxygen Conc.", unit: "mg/L", value: data.oxygenConcentration, isOutOfBounds: outOfBoundsKeys.contains('oxygenConcentration')),
|
||||||
_buildParameterListItem(context, icon: Icons.science_outlined, label: "pH", unit: "", value: data.ph?.toStringAsFixed(2)),
|
_buildParameterListItem(context, icon: Icons.percent, label: "Oxygen Sat.", unit: "%", value: data.oxygenSaturation, isOutOfBounds: outOfBoundsKeys.contains('oxygenSaturation')),
|
||||||
_buildParameterListItem(context, icon: Icons.waves, label: "Salinity", unit: "ppt", value: data.salinity?.toStringAsFixed(2)),
|
_buildParameterListItem(context, icon: Icons.science_outlined, label: "pH", unit: "", value: data.ph, isOutOfBounds: outOfBoundsKeys.contains('ph')),
|
||||||
_buildParameterListItem(context, icon: Icons.flash_on, label: "Conductivity", unit: "µS/cm", value: data.electricalConductivity?.toStringAsFixed(0)),
|
_buildParameterListItem(context, icon: Icons.waves, label: "Salinity", unit: "ppt", value: data.salinity, isOutOfBounds: outOfBoundsKeys.contains('salinity')),
|
||||||
_buildParameterListItem(context, icon: Icons.thermostat, label: "Temperature", unit: "°C", value: data.temperature?.toStringAsFixed(2)),
|
_buildParameterListItem(context, icon: Icons.flash_on, label: "Conductivity", unit: "µS/cm", value: data.electricalConductivity, isOutOfBounds: outOfBoundsKeys.contains('electricalConductivity')),
|
||||||
_buildParameterListItem(context, icon: Icons.grain, label: "TDS", unit: "mg/L", value: data.tds?.toStringAsFixed(2)),
|
_buildParameterListItem(context, icon: Icons.thermostat, label: "Temperature", unit: "°C", value: data.temperature, isOutOfBounds: outOfBoundsKeys.contains('temperature')),
|
||||||
_buildParameterListItem(context, icon: Icons.opacity, label: "Turbidity", unit: "NTU", value: data.turbidity?.toStringAsFixed(2)),
|
_buildParameterListItem(context, icon: Icons.grain, label: "TDS", unit: "mg/L", value: data.tds, isOutOfBounds: outOfBoundsKeys.contains('tds')),
|
||||||
_buildParameterListItem(context, icon: Icons.science, label: "Ammonia", unit: "mg/L", value: data.ammonia?.toStringAsFixed(2)), // MODIFIED: Replaced TSS with Ammonia
|
_buildParameterListItem(context, icon: Icons.opacity, label: "Turbidity", unit: "NTU", value: data.turbidity, isOutOfBounds: outOfBoundsKeys.contains('turbidity')),
|
||||||
_buildParameterListItem(context, icon: Icons.battery_charging_full, label: "Battery", unit: "V", value: data.batteryVoltage?.toStringAsFixed(2)),
|
_buildParameterListItem(context, icon: Icons.science, label: "Ammonia", unit: "mg/L", value: data.ammonia, isOutOfBounds: outOfBoundsKeys.contains('ammonia')),
|
||||||
|
_buildParameterListItem(context, icon: Icons.battery_charging_full, label: "Battery", unit: "V", value: data.batteryVoltage, isOutOfBounds: outOfBoundsKeys.contains('batteryVoltage')),
|
||||||
|
// --- END: MODIFICATION ---
|
||||||
const Divider(height: 20),
|
const Divider(height: 20),
|
||||||
_buildFlowrateSummary(context),
|
_buildFlowrateSummary(context),
|
||||||
],
|
],
|
||||||
@ -176,9 +244,17 @@ class RiverInSituStep5Summary extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildParameterListItem(BuildContext context, {required IconData icon, required String label, required String unit, required String? value}) {
|
// --- START: MODIFICATION FOR 5 DECIMALS & HIGHLIGHTING ---
|
||||||
final bool isMissing = value == null || value.contains('-999');
|
Widget _buildParameterListItem(BuildContext context, {required IconData icon, required String label, required String unit, required double? value, bool isOutOfBounds = false}) {
|
||||||
final String displayValue = isMissing ? 'N/A' : '$value ${unit}'.trim();
|
final bool isMissing = value == null || value == -999.0;
|
||||||
|
// Format the value to 5 decimal places if it's a valid number.
|
||||||
|
final String displayValue = isMissing ? 'N/A' : '${value.toStringAsFixed(5)} ${unit}'.trim();
|
||||||
|
|
||||||
|
// Determine the color for the value based on theme and status.
|
||||||
|
final Color? defaultTextColor = Theme.of(context).textTheme.bodyLarge?.color;
|
||||||
|
final Color valueColor = isOutOfBounds
|
||||||
|
? Colors.red
|
||||||
|
: (isMissing ? Colors.grey : defaultTextColor ?? Colors.black);
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
@ -188,12 +264,13 @@ class RiverInSituStep5Summary extends StatelessWidget {
|
|||||||
trailing: Text(
|
trailing: Text(
|
||||||
displayValue,
|
displayValue,
|
||||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
color: isMissing ? Colors.grey : null,
|
color: valueColor,
|
||||||
fontWeight: isMissing ? null : FontWeight.bold,
|
fontWeight: isOutOfBounds ? FontWeight.bold : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFICATION ---
|
||||||
|
|
||||||
Widget _buildImageCard(String title, File? image, {String? remark}) {
|
Widget _buildImageCard(String title, File? image, {String? remark}) {
|
||||||
final bool hasRemark = remark != null && remark.isNotEmpty;
|
final bool hasRemark = remark != null && remark.isNotEmpty;
|
||||||
@ -229,7 +306,6 @@ class RiverInSituStep5Summary extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIX: Reorganized the widget for a cleaner and more beautiful presentation.
|
|
||||||
Widget _buildFlowrateSummary(BuildContext context) {
|
Widget _buildFlowrateSummary(BuildContext context) {
|
||||||
final method = data.flowrateMethod ?? 'N/A';
|
final method = data.flowrateMethod ?? 'N/A';
|
||||||
|
|
||||||
|
|||||||
@ -650,7 +650,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.info_outline),
|
leading: const Icon(Icons.info_outline),
|
||||||
title: const Text('App Version'),
|
title: const Text('App Version'),
|
||||||
subtitle: const Text('1.2.03'),
|
subtitle: const Text('MMS V4 1.2.07'),
|
||||||
dense: true,
|
dense: true,
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|||||||
@ -330,6 +330,66 @@ class ApiService {
|
|||||||
return {'success': false, 'message': 'Data sync failed: $e'};
|
return {'success': false, 'message': 'Data sync failed: $e'};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START: NEW METHOD FOR REGISTRATION SCREEN ---
|
||||||
|
/// Fetches only the public master data required for the registration screen.
|
||||||
|
Future<Map<String, dynamic>> syncRegistrationData() async {
|
||||||
|
debugPrint('ApiService: Starting registration data sync...');
|
||||||
|
try {
|
||||||
|
// Define only the tasks needed for registration
|
||||||
|
final syncTasks = {
|
||||||
|
'departments': {
|
||||||
|
'endpoint': 'departments',
|
||||||
|
'handler': (d, id) async {
|
||||||
|
await dbHelper.upsertDepartments(d);
|
||||||
|
await dbHelper.deleteDepartments(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'companies': {
|
||||||
|
'endpoint': 'companies',
|
||||||
|
'handler': (d, id) async {
|
||||||
|
await dbHelper.upsertCompanies(d);
|
||||||
|
await dbHelper.deleteCompanies(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'positions': {
|
||||||
|
'endpoint': 'positions',
|
||||||
|
'handler': (d, id) async {
|
||||||
|
await dbHelper.upsertPositions(d);
|
||||||
|
await dbHelper.deletePositions(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch all deltas in parallel, always a full fetch (since = null)
|
||||||
|
final fetchFutures = syncTasks.map((key, value) =>
|
||||||
|
MapEntry(key, _fetchDelta(value['endpoint'] as String, null)));
|
||||||
|
final results = await Future.wait(fetchFutures.values);
|
||||||
|
final resultData = Map.fromIterables(fetchFutures.keys, results);
|
||||||
|
|
||||||
|
// Process and save all changes
|
||||||
|
for (var entry in resultData.entries) {
|
||||||
|
final key = entry.key;
|
||||||
|
final result = entry.value;
|
||||||
|
|
||||||
|
if (result['success'] == true && result['data'] != null) {
|
||||||
|
final updated = List<Map<String, dynamic>>.from(result['data']['updated'] ?? []);
|
||||||
|
final deleted = List<dynamic>.from(result['data']['deleted'] ?? []);
|
||||||
|
await (syncTasks[key]!['handler'] as Function)(updated, deleted);
|
||||||
|
} else {
|
||||||
|
debugPrint('ApiService: Failed to sync $key. Message: ${result['message']}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('ApiService: Registration data sync complete.');
|
||||||
|
return {'success': true, 'message': 'Registration data sync successful.'};
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('ApiService: Registration data sync failed: $e');
|
||||||
|
return {'success': false, 'message': 'Registration data sync failed: $e'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END: NEW METHOD FOR REGISTRATION SCREEN ---
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user