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" />
|
||||
<!-- END: STORAGE PERMISSIONS -->
|
||||
|
||||
<!-- MMS V4 1.2.05 -->
|
||||
<!-- MMS V4 1.2.06 -->
|
||||
<application
|
||||
android:label="MMS V4 debug"
|
||||
android:label="MMS V4 1.2.07"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
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 {
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
if (connectivityResult.contains(ConnectivityResult.none)) {
|
||||
|
||||
@ -8,9 +8,7 @@ import 'package:simple_barcode_scanner/simple_barcode_scanner.dart';
|
||||
|
||||
import '../../../../auth_provider.dart';
|
||||
import '../../../../models/in_situ_sampling_data.dart';
|
||||
// START CHANGE: Import the new, correct service file
|
||||
import '../../../../services/marine_in_situ_sampling_service.dart';
|
||||
// END CHANGE
|
||||
|
||||
class InSituStep1SamplingInfo extends StatefulWidget {
|
||||
final InSituSamplingData data;
|
||||
@ -90,6 +88,10 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
||||
_dateController.text = widget.data.samplingDate!;
|
||||
_timeController.text = widget.data.samplingTime!;
|
||||
|
||||
if (widget.data.samplingType == null) {
|
||||
widget.data.samplingType = 'Schedule';
|
||||
}
|
||||
|
||||
final allStations = auth.manualStations ?? [];
|
||||
if (allStations.isNotEmpty) {
|
||||
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) =>
|
||||
s['state_name'] == widget.data.selectedStateName &&
|
||||
s['category_name'] == widget.data.selectedCategoryName)
|
||||
.toList();
|
||||
.toList()
|
||||
..sort((a, b) => (a['man_station_code'] ?? '').compareTo(b['man_station_code'] ?? ''));
|
||||
}
|
||||
|
||||
setState(() {
|
||||
@ -121,9 +124,7 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
||||
|
||||
Future<void> _getCurrentLocation() async {
|
||||
setState(() => _isLoadingLocation = true);
|
||||
// START CHANGE: Use the correct, new service type from Provider
|
||||
final service = Provider.of<MarineInSituSamplingService>(context, listen: false);
|
||||
// END CHANGE
|
||||
|
||||
try {
|
||||
final position = await service.getCurrentLocation();
|
||||
@ -154,9 +155,7 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
||||
final lon2Str = widget.data.currentLongitude;
|
||||
|
||||
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);
|
||||
// END CHANGE
|
||||
final lat1 = double.tryParse(lat1Str);
|
||||
final lon1 = double.tryParse(lon1Str);
|
||||
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() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_formKey.currentState!.save();
|
||||
|
||||
final distanceInMeters = (widget.data.distanceDifferenceInKm ?? 0) * 1000;
|
||||
|
||||
if (distanceInMeters > 700) {
|
||||
if (distanceInMeters > 50) {
|
||||
_showDistanceRemarkDialog();
|
||||
} else {
|
||||
// If distance is okay, clear any previous remarks and proceed.
|
||||
widget.data.distanceDifferenceRemarks = null;
|
||||
widget.onNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a dialog to force the user to enter remarks for large distance differences.
|
||||
Future<void> _showDistanceRemarkDialog() async {
|
||||
final remarkController = TextEditingController(text: widget.data.distanceDifferenceRemarks);
|
||||
final dialogFormKey = GlobalKey<FormState>();
|
||||
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false, // User must interact with the dialog
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Distance Warning'),
|
||||
@ -219,7 +300,7 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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),
|
||||
TextFormField(
|
||||
controller: remarkController,
|
||||
@ -255,7 +336,7 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
||||
widget.data.distanceDifferenceRemarks = remarkController.text;
|
||||
});
|
||||
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 allStations = auth.manualStations ?? [];
|
||||
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(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
children: [
|
||||
// Sampling Information section...
|
||||
Text("Sampling Information", style: Theme.of(context).textTheme.headlineSmall),
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(controller: _firstSamplerController, readOnly: true, decoration: const InputDecoration(labelText: '1st Sampler')),
|
||||
@ -308,7 +389,6 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Station Selection section...
|
||||
Text("Station Selection", style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 16),
|
||||
DropdownSearch<String>(
|
||||
@ -355,7 +435,12 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
||||
_stationLatController.clear();
|
||||
_stationLonController.clear();
|
||||
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,
|
||||
@ -382,9 +467,20 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
||||
TextFormField(controller: _stationLatController, readOnly: true, decoration: const InputDecoration(labelText: 'Station Latitude')),
|
||||
const SizedBox(height: 16),
|
||||
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),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(controller: _currentLatController, readOnly: true, decoration: const InputDecoration(labelText: 'Current Latitude')),
|
||||
@ -396,9 +492,9 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
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),
|
||||
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(
|
||||
textAlign: TextAlign.center,
|
||||
@ -410,7 +506,7 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
||||
text: '${(widget.data.distanceDifferenceInKm! * 1000).toStringAsFixed(0)} meters',
|
||||
style: TextStyle(
|
||||
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 '../../../../models/in_situ_sampling_data.dart';
|
||||
// START CHANGE: Import the new, correct service file
|
||||
import '../../../../services/marine_in_situ_sampling_service.dart';
|
||||
// END CHANGE
|
||||
|
||||
/// The second step of the In-Situ Sampling form.
|
||||
/// Gathers on-site conditions (weather, tide) and handles all photo attachments.
|
||||
@ -30,13 +28,10 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isPickingImage = false;
|
||||
|
||||
// --- UI Controllers for remarks ---
|
||||
// --- START MODIFICATION: Removed optional remark controllers ---
|
||||
late final TextEditingController _eventRemarksController;
|
||||
late final TextEditingController _labRemarksController;
|
||||
late final TextEditingController _optionalRemark1Controller;
|
||||
late final TextEditingController _optionalRemark2Controller;
|
||||
late final TextEditingController _optionalRemark3Controller;
|
||||
late final TextEditingController _optionalRemark4Controller;
|
||||
// --- END MODIFICATION ---
|
||||
|
||||
|
||||
final List<String> _weatherOptions = ['Clear', 'Rainy', 'Cloudy'];
|
||||
@ -48,20 +43,16 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
||||
super.initState();
|
||||
_eventRemarksController = TextEditingController(text: widget.data.eventRemarks);
|
||||
_labRemarksController = TextEditingController(text: widget.data.labRemarks);
|
||||
_optionalRemark1Controller = TextEditingController(text: widget.data.optionalRemark1);
|
||||
_optionalRemark2Controller = TextEditingController(text: widget.data.optionalRemark2);
|
||||
_optionalRemark3Controller = TextEditingController(text: widget.data.optionalRemark3);
|
||||
_optionalRemark4Controller = TextEditingController(text: widget.data.optionalRemark4);
|
||||
// --- START MODIFICATION: Removed initialization for optional remark controllers ---
|
||||
// --- END MODIFICATION ---
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_eventRemarksController.dispose();
|
||||
_labRemarksController.dispose();
|
||||
_optionalRemark1Controller.dispose();
|
||||
_optionalRemark2Controller.dispose();
|
||||
_optionalRemark3Controller.dispose();
|
||||
_optionalRemark4Controller.dispose();
|
||||
// --- START MODIFICATION: Removed disposal of optional remark controllers ---
|
||||
// --- END MODIFICATION ---
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -70,9 +61,7 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
||||
if (_isPickingImage) return;
|
||||
setState(() => _isPickingImage = true);
|
||||
|
||||
// START CHANGE: Use the correct service type from Provider
|
||||
final service = Provider.of<MarineInSituSamplingService>(context, listen: false);
|
||||
// END CHANGE
|
||||
|
||||
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.
|
||||
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()) {
|
||||
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();
|
||||
// --- FIXED: Correctly save remarks text to the data model's remark properties ---
|
||||
widget.data.optionalRemark1 = _optionalRemark1Controller.text;
|
||||
widget.data.optionalRemark2 = _optionalRemark2Controller.text;
|
||||
widget.data.optionalRemark3 = _optionalRemark3Controller.text;
|
||||
widget.data.optionalRemark4 = _optionalRemark4Controller.text;
|
||||
|
||||
// Removed saving of optional remarks as they are no longer present
|
||||
widget.onNext();
|
||||
// --- END MODIFICATION ---
|
||||
}
|
||||
|
||||
void _showSnackBar(String message, {bool isError = false}) {
|
||||
@ -122,6 +110,14 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
||||
|
||||
@override
|
||||
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(
|
||||
key: _formKey,
|
||||
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('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('Examine Preservative (pH paper)', 'PH_PAPER', widget.data.phPaperImage, (file) => widget.data.phPaperImage = file, isRequired: true),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// --- Section: Optional Photos ---
|
||||
Text("Optional Photos & Remarks", style: Theme.of(context).textTheme.titleLarge),
|
||||
// --- START MODIFICATION: Section for additional photos and conditional remarks ---
|
||||
Text("Additional Photos & Remarks", style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 8),
|
||||
_buildImagePicker('Optional Photo 1', 'OPTIONAL_1', widget.data.optionalImage1, (file) => widget.data.optionalImage1 = file, remarkController: _optionalRemark1Controller, isRequired: false),
|
||||
_buildImagePicker('Optional Photo 2', 'OPTIONAL_2', widget.data.optionalImage2, (file) => widget.data.optionalImage2 = file, remarkController: _optionalRemark2Controller, isRequired: false),
|
||||
_buildImagePicker('Optional Photo 3', 'OPTIONAL_3', widget.data.optionalImage3, (file) => widget.data.optionalImage3 = file, remarkController: _optionalRemark3Controller, isRequired: false),
|
||||
_buildImagePicker('Optional Photo 4', 'OPTIONAL_4', widget.data.optionalImage4, (file) => widget.data.optionalImage4 = file, remarkController: _optionalRemark4Controller, isRequired: false),
|
||||
// pH Paper photo is now the first optional photo
|
||||
_buildImagePicker('Examine Preservative (pH paper)', 'PH_PAPER', widget.data.phPaperImage, (file) => widget.data.phPaperImage = file, isRequired: false),
|
||||
// Other optional photos no longer have remark fields
|
||||
_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),
|
||||
|
||||
// --- Section: Remarks ---
|
||||
Text("Remarks", style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 16),
|
||||
// Event Remarks field is now conditionally required
|
||||
TextFormField(
|
||||
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,
|
||||
validator: (value) {
|
||||
if (areAdditionalPhotosAttached && (value == null || value.trim().isEmpty)) {
|
||||
return 'Event Remarks are required when attaching additional photos.';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
maxLines: 3,
|
||||
),
|
||||
// --- END MODIFICATION ---
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _labRemarksController,
|
||||
@ -203,7 +211,9 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
||||
}
|
||||
|
||||
/// 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(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
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")),
|
||||
],
|
||||
),
|
||||
if (remarkController != null)
|
||||
Padding(
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// --- START MODIFICATION: Removed remark text field ---
|
||||
// --- END MODIFICATION ---
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// lib/screens/register.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -33,6 +35,20 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
int? _selectedCompanyId;
|
||||
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
|
||||
void dispose() {
|
||||
_usernameController.dispose();
|
||||
@ -120,6 +136,24 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
final companies = auth.companies ?? [];
|
||||
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(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
|
||||
@ -36,12 +36,10 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
||||
late final TextEditingController _stationLonController;
|
||||
late final TextEditingController _currentLatController;
|
||||
late final TextEditingController _currentLonController;
|
||||
// REMOVED: Controllers for weather and remarks.
|
||||
|
||||
List<String> _statesList = [];
|
||||
List<Map<String, dynamic>> _stationsForState = [];
|
||||
final List<String> _samplingTypes = ['Schedule', 'Triennial'];
|
||||
// REMOVED: Weather options list.
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -60,7 +58,6 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
||||
_stationLonController.dispose();
|
||||
_currentLatController.dispose();
|
||||
_currentLonController.dispose();
|
||||
// REMOVED: Dispose controllers for remarks.
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -73,7 +70,6 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
||||
_stationLonController = TextEditingController(text: widget.data.stationLongitude);
|
||||
_currentLatController = TextEditingController(text: widget.data.currentLatitude);
|
||||
_currentLonController = TextEditingController(text: widget.data.currentLongitude);
|
||||
// REMOVED: Initialize controllers for remarks.
|
||||
}
|
||||
|
||||
void _initializeForm() {
|
||||
@ -91,6 +87,10 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
||||
_dateController.text = widget.data.samplingDate!;
|
||||
_timeController.text = widget.data.samplingTime!;
|
||||
|
||||
if (widget.data.samplingType == null) {
|
||||
widget.data.samplingType = 'Schedule';
|
||||
}
|
||||
|
||||
final allStations = auth.riverManualStations ?? [];
|
||||
if (allStations.isNotEmpty) {
|
||||
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) {
|
||||
_stationsForState = allStations
|
||||
.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(() {
|
||||
@ -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() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_formKey.currentState!.save();
|
||||
|
||||
final distanceInMeters = (widget.data.distanceDifferenceInKm ?? 0) * 1000;
|
||||
|
||||
if (distanceInMeters > 700) {
|
||||
if (distanceInMeters > 50) {
|
||||
_showDistanceRemarkDialog();
|
||||
} else {
|
||||
widget.data.distanceDifferenceRemarks = null;
|
||||
@ -201,7 +266,7 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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),
|
||||
TextFormField(
|
||||
controller: remarkController,
|
||||
@ -252,7 +317,11 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
final allStations = auth.riverManualStations ?? [];
|
||||
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(
|
||||
key: _formKey,
|
||||
@ -318,9 +387,12 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
||||
widget.data.distanceDifferenceInKm = null;
|
||||
|
||||
_stationsForState = state != null
|
||||
? allStations
|
||||
? (allStations
|
||||
.where((s) => s['state_name'] == state)
|
||||
.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')),
|
||||
const SizedBox(height: 16),
|
||||
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),
|
||||
|
||||
Text("Location Verification", style: Theme.of(context).textTheme.titleLarge),
|
||||
@ -368,9 +451,9 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
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),
|
||||
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(
|
||||
textAlign: TextAlign.center,
|
||||
@ -382,7 +465,7 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
||||
text: '${(widget.data.distanceDifferenceInKm! * 1000).toStringAsFixed(0)} meters',
|
||||
style: TextStyle(
|
||||
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),
|
||||
|
||||
// REMOVED: On-Site Information section (Weather, Remarks).
|
||||
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
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 _labRemarksController;
|
||||
final List<String> _weatherOptions = ['Clear', 'Rainy', 'Cloudy'];
|
||||
final List<String> _weatherOptions = ['Cloudy', 'Drizzle', 'Rainy', 'Sunny', 'Windy'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
@ -35,8 +35,13 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
bool _isAutoReading = false;
|
||||
StreamSubscription? _dataSubscription;
|
||||
|
||||
// --- START FIX: Declare service variable for safe disposal ---
|
||||
late final RiverInSituSamplingService _samplingService;
|
||||
// --- END FIX ---
|
||||
|
||||
// --- START: Added for Parameter Validation Feature ---
|
||||
Map<String, double>? _previousReadingsForComparison;
|
||||
Set<String> _outOfBoundsKeys = {};
|
||||
|
||||
final Map<String, String> _parameterKeyToLimitName = const {
|
||||
'oxygenConcentration': 'Oxygen Conc',
|
||||
@ -48,6 +53,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
'tds': 'TDS',
|
||||
'turbidity': 'Turbidity',
|
||||
'ammonia': 'Ammonia',
|
||||
'batteryVoltage': 'Battery',
|
||||
};
|
||||
// --- END: Added for Parameter Validation Feature ---
|
||||
|
||||
@ -79,6 +85,9 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// --- START FIX: Initialize service variable safely ---
|
||||
_samplingService = Provider.of<RiverInSituSamplingService>(context, listen: false);
|
||||
// --- END FIX ---
|
||||
_initializeControllers();
|
||||
_initializeFlowrateControllers();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
@ -87,6 +96,16 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
@override
|
||||
void dispose() {
|
||||
_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();
|
||||
_disposeFlowrateControllers();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
@ -123,7 +142,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
|
||||
if (_parameters.isEmpty) {
|
||||
_parameters.addAll([
|
||||
// --- START: Added 'key' for programmatic access ---
|
||||
{'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': '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': 'ammonia', 'icon': Icons.science, 'label': 'Ammonia', 'unit': 'mg/L', 'controller': _ammoniaController},
|
||||
{'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 outOfBoundsParams = _validateParameters(currentReadings, riverLimits);
|
||||
|
||||
setState(() {
|
||||
_outOfBoundsKeys = outOfBoundsParams.map((p) => _parameters.firstWhere((param) => param['label'] == p['label'])['key'] as String).toSet();
|
||||
});
|
||||
|
||||
if (outOfBoundsParams.isNotEmpty) {
|
||||
_showParameterLimitDialog(outOfBoundsParams, currentReadings);
|
||||
} else {
|
||||
@ -445,11 +466,12 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
return;
|
||||
}
|
||||
|
||||
if (_previousReadingsForComparison != null) {
|
||||
setState(() {
|
||||
setState(() {
|
||||
_outOfBoundsKeys.clear();
|
||||
if (_previousReadingsForComparison != null) {
|
||||
_previousReadingsForComparison = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
widget.onNext();
|
||||
}
|
||||
@ -549,11 +571,11 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
Expanded(child: TextFormField(controller: _timeController, readOnly: true, decoration: const InputDecoration(labelText: 'Time'))),
|
||||
],
|
||||
),
|
||||
const Divider(height: 32),
|
||||
|
||||
if (_previousReadingsForComparison != null)
|
||||
_buildComparisonView(),
|
||||
|
||||
const Divider(height: 32),
|
||||
Column(
|
||||
children: _parameters.map((param) {
|
||||
return _buildParameterListItem(
|
||||
@ -561,6 +583,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
label: param['label'] as String,
|
||||
unit: param['unit'] as String,
|
||||
controller: param['controller'] as TextEditingController,
|
||||
isOutOfBounds: _outOfBoundsKeys.contains(param['key']),
|
||||
);
|
||||
}).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 String displayValue = isMissing ? '-.--' : controller.text;
|
||||
final String displayLabel = unit.isEmpty ? label : '$label ($unit)';
|
||||
|
||||
final Color valueColor = isOutOfBounds
|
||||
? Colors.red
|
||||
: (isMissing ? Colors.grey : Theme.of(context).colorScheme.primary);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: ListTile(
|
||||
@ -590,7 +618,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
displayValue,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
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;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
margin: const EdgeInsets.only(top: 24.0),
|
||||
color: Theme.of(context).cardColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
@ -688,6 +716,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
final label = param['label'] as String;
|
||||
final controller = param['controller'] as TextEditingController;
|
||||
final previousValue = previousReadings[key];
|
||||
final bool isCurrentValueOutOfBounds = _outOfBoundsKeys.contains(key);
|
||||
|
||||
return TableRow(
|
||||
children: [
|
||||
@ -695,15 +724,20 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
previousValue == -999.0 ? '-.--' : previousValue!.toStringAsFixed(2),
|
||||
previousValue == -999.0 ? '-.--' : previousValue!.toStringAsFixed(5),
|
||||
style: TextStyle(color: isDarkTheme ? Colors.orange.shade200 : Colors.orange.shade700),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
controller.text.contains('-999') ? '-.--' : (double.tryParse(controller.text) ?? 0).toStringAsFixed(2),
|
||||
style: TextStyle(color: isDarkTheme ? Colors.green.shade200 : Colors.green.shade700, fontWeight: FontWeight.bold),
|
||||
controller.text.contains('-999') ? '-.--' : (double.tryParse(controller.text) ?? 0).toStringAsFixed(5),
|
||||
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(
|
||||
children: [
|
||||
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: const EdgeInsets.all(6.0),
|
||||
child: Text(
|
||||
p['value'].toStringAsFixed(2),
|
||||
p['value'].toStringAsFixed(5),
|
||||
style: const TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
@ -772,7 +806,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
],
|
||||
),
|
||||
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 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../../auth_provider.dart';
|
||||
import '../../../../models/river_in_situ_sampling_data.dart';
|
||||
|
||||
class RiverInSituStep5Summary extends StatelessWidget {
|
||||
@ -17,8 +19,73 @@ class RiverInSituStep5Summary extends StatelessWidget {
|
||||
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
|
||||
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(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
@ -92,17 +159,18 @@ class RiverInSituStep5Summary extends StatelessWidget {
|
||||
_buildDetailRow("Sonde ID:", data.sondeId),
|
||||
_buildDetailRow("Capture Time:", "${data.dataCaptureDate} ${data.dataCaptureTime}"),
|
||||
const Divider(height: 20),
|
||||
_buildParameterListItem(context, icon: Icons.air, label: "Oxygen Conc.", unit: "mg/L", value: data.oxygenConcentration?.toStringAsFixed(2)),
|
||||
_buildParameterListItem(context, icon: Icons.percent, label: "Oxygen Sat.", unit: "%", value: data.oxygenSaturation?.toStringAsFixed(2)),
|
||||
_buildParameterListItem(context, icon: Icons.science_outlined, label: "pH", unit: "", value: data.ph?.toStringAsFixed(2)),
|
||||
_buildParameterListItem(context, icon: Icons.waves, label: "Salinity", unit: "ppt", value: data.salinity?.toStringAsFixed(2)),
|
||||
_buildParameterListItem(context, icon: Icons.flash_on, label: "Conductivity", unit: "µS/cm", value: data.electricalConductivity?.toStringAsFixed(0)),
|
||||
_buildParameterListItem(context, icon: Icons.thermostat, label: "Temperature", unit: "°C", value: data.temperature?.toStringAsFixed(2)),
|
||||
_buildParameterListItem(context, icon: Icons.grain, label: "TDS", unit: "mg/L", value: data.tds?.toStringAsFixed(2)),
|
||||
_buildParameterListItem(context, icon: Icons.opacity, label: "Turbidity", unit: "NTU", value: data.turbidity?.toStringAsFixed(2)),
|
||||
_buildParameterListItem(context, icon: Icons.science, label: "Ammonia", unit: "mg/L", value: data.ammonia?.toStringAsFixed(2)), // MODIFIED: Replaced TSS with Ammonia
|
||||
_buildParameterListItem(context, icon: Icons.battery_charging_full, label: "Battery", unit: "V", value: data.batteryVoltage?.toStringAsFixed(2)),
|
||||
|
||||
// --- START: MODIFICATION FOR 5 DECIMALS & HIGHLIGHTING ---
|
||||
_buildParameterListItem(context, icon: Icons.air, label: "Oxygen Conc.", unit: "mg/L", value: data.oxygenConcentration, isOutOfBounds: outOfBoundsKeys.contains('oxygenConcentration')),
|
||||
_buildParameterListItem(context, icon: Icons.percent, label: "Oxygen Sat.", unit: "%", value: data.oxygenSaturation, isOutOfBounds: outOfBoundsKeys.contains('oxygenSaturation')),
|
||||
_buildParameterListItem(context, icon: Icons.science_outlined, label: "pH", unit: "", value: data.ph, isOutOfBounds: outOfBoundsKeys.contains('ph')),
|
||||
_buildParameterListItem(context, icon: Icons.waves, label: "Salinity", unit: "ppt", value: data.salinity, isOutOfBounds: outOfBoundsKeys.contains('salinity')),
|
||||
_buildParameterListItem(context, icon: Icons.flash_on, label: "Conductivity", unit: "µS/cm", value: data.electricalConductivity, isOutOfBounds: outOfBoundsKeys.contains('electricalConductivity')),
|
||||
_buildParameterListItem(context, icon: Icons.thermostat, label: "Temperature", unit: "°C", value: data.temperature, isOutOfBounds: outOfBoundsKeys.contains('temperature')),
|
||||
_buildParameterListItem(context, icon: Icons.grain, label: "TDS", unit: "mg/L", value: data.tds, isOutOfBounds: outOfBoundsKeys.contains('tds')),
|
||||
_buildParameterListItem(context, icon: Icons.opacity, label: "Turbidity", unit: "NTU", value: data.turbidity, isOutOfBounds: outOfBoundsKeys.contains('turbidity')),
|
||||
_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),
|
||||
_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}) {
|
||||
final bool isMissing = value == null || value.contains('-999');
|
||||
final String displayValue = isMissing ? 'N/A' : '$value ${unit}'.trim();
|
||||
// --- START: MODIFICATION FOR 5 DECIMALS & HIGHLIGHTING ---
|
||||
Widget _buildParameterListItem(BuildContext context, {required IconData icon, required String label, required String unit, required double? value, bool isOutOfBounds = false}) {
|
||||
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(
|
||||
dense: true,
|
||||
@ -188,12 +264,13 @@ class RiverInSituStep5Summary extends StatelessWidget {
|
||||
trailing: Text(
|
||||
displayValue,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: isMissing ? Colors.grey : null,
|
||||
fontWeight: isMissing ? null : FontWeight.bold,
|
||||
color: valueColor,
|
||||
fontWeight: isOutOfBounds ? FontWeight.bold : null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// --- END: MODIFICATION ---
|
||||
|
||||
Widget _buildImageCard(String title, File? image, {String? remark}) {
|
||||
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) {
|
||||
final method = data.flowrateMethod ?? 'N/A';
|
||||
|
||||
|
||||
@ -650,7 +650,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('App Version'),
|
||||
subtitle: const Text('1.2.03'),
|
||||
subtitle: const Text('MMS V4 1.2.07'),
|
||||
dense: true,
|
||||
),
|
||||
ListTile(
|
||||
|
||||
@ -330,6 +330,66 @@ class ApiService {
|
||||
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