repair on the register screen

This commit is contained in:
ALim Aidrus 2025-10-02 10:51:06 +08:00
parent f44245fb5a
commit 31b64fc203
11 changed files with 620 additions and 128 deletions

View File

@ -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">

View File

@ -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)) {

View File

@ -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
),
),
],
@ -450,3 +546,49 @@ 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 ---

View File

@ -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 ---
],
),
);

View File

@ -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(

View File

@ -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,
@ -411,3 +492,47 @@ 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'),
),
],
);
}
}

View File

@ -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() {

View File

@ -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(() {
_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.'),
],
),
),

View File

@ -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';

View File

@ -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(

View File

@ -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 ---
}
// =======================================================================