diff --git a/lib/home_page.dart b/lib/home_page.dart index 541d81e..0ac68d0 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -29,7 +29,7 @@ class _HomePageState extends State { }); }, ), - title: const Text("MMS Version 3.4.01"), + title: const Text("MMS Version 3.5.01"), actions: [ IconButton( icon: const Icon(Icons.person), diff --git a/lib/screens/marine/manual/tarball_sampling_step2.dart b/lib/screens/marine/manual/tarball_sampling_step2.dart index 3925863..4dfc78f 100644 --- a/lib/screens/marine/manual/tarball_sampling_step2.dart +++ b/lib/screens/marine/manual/tarball_sampling_step2.dart @@ -25,9 +25,14 @@ class _TarballSamplingStep2State extends State { final _formKey = GlobalKey(); bool _isPickingImage = false; - // This will hold the user's selection in the UI Map? _selectedClassification; + // --- START: MODIFICATION 1 --- + // Added a state variable to easily check if the classification requires photos. + // We will assume 'None' classification has an ID of 1. Adjust if necessary. + bool _isNoneClassificationSelected = false; + // --- END: MODIFICATION 1 --- + late final TextEditingController _remark1Controller; late final TextEditingController _remark2Controller; late final TextEditingController _remark3Controller; @@ -42,11 +47,9 @@ class _TarballSamplingStep2State extends State { _remark3Controller = TextEditingController(text: widget.data.optionalRemark3); _remark4Controller = TextEditingController(text: widget.data.optionalRemark4); - // This block ensures the dropdown shows the correct value if the user comes back to this screen WidgetsBinding.instance.addPostFrameCallback((_) { final auth = Provider.of(context, listen: false); - // Restore the selected value from the data model using the cached list in the provider if (widget.data.classificationId != null && auth.tarballClassifications != null) { try { final foundClassification = auth.tarballClassifications!.firstWhere( @@ -54,19 +57,16 @@ class _TarballSamplingStep2State extends State { ); setState(() { _selectedClassification = foundClassification; - // Also restore the full object to the data model widget.data.selectedClassification = foundClassification; + // --- START: MODIFICATION 2 --- + // Update our state variable when restoring the form state. + _isNoneClassificationSelected = widget.data.classificationId == 1; + // --- END: MODIFICATION 2 --- }); } catch (e) { debugPrint("Could not find pre-selected classification in the cached list."); } } - - // **OFFLINE-FIRST SYNC**: - // Attempt to sync all master data with the server in the background. - // The UI will build instantly using existing local data from AuthProvider. - // If the sync is successful, the Consumer widget will automatically rebuild the dropdown with fresh data. - // If offline, this will fail gracefully and the user will see the data from the last successful sync. auth.syncAllData(); }); } @@ -86,10 +86,7 @@ class _TarballSamplingStep2State extends State { builder: (BuildContext context) { return AlertDialog( title: const Text("Incorrect Image Orientation"), - // --- START: MODIFICATION 1 --- - // Updated the dialog text to be more general as it now applies to all photos. content: const Text("All photos must be taken in a horizontal (landscape) orientation."), - // --- END: MODIFICATION 1 --- actions: [ TextButton( child: const Text("OK"), @@ -101,11 +98,7 @@ class _TarballSamplingStep2State extends State { ); } - // --- START: MODIFICATION 2 --- - // The `isRequired` parameter has been removed. The orientation check will now - // apply to every image processed by this function. Future _pickAndProcessImage(ImageSource source, String imageInfo) async { - // --- END: MODIFICATION 2 --- if (_isPickingImage) return null; setState(() => _isPickingImage = true); @@ -124,15 +117,11 @@ class _TarballSamplingStep2State extends State { return null; } - // --- START: MODIFICATION 3 --- - // The `isRequired` check has been removed. Now, ALL photos (required and optional) - // must be in landscape orientation (width > height). if (originalImage.height > originalImage.width) { _showOrientationDialog(); setState(() => _isPickingImage = false); return null; } - // --- END: MODIFICATION 3 --- final String watermarkTimestamp = "${widget.data.samplingDate} ${widget.data.samplingTime}"; final font = img.arial24; @@ -172,12 +161,8 @@ class _TarballSamplingStep2State extends State { return processedFile; } - // --- START: MODIFICATION 4 --- - // The `isRequired` parameter has been removed from the function signature - // to align with the changes in `_pickAndProcessImage`. void _setImage(Function(File?) setImageCallback, ImageSource source, String imageInfo) async { final file = await _pickAndProcessImage(source, imageInfo); - // --- END: MODIFICATION 4 --- if (file != null) { setState(() { setImageCallback(file); @@ -210,9 +195,23 @@ class _TarballSamplingStep2State extends State { return; } - // --- START: MODIFICATION 5 --- - // Added validation to ensure that if an optional image is provided, its - // corresponding remark field is not empty. + // --- START: MODIFICATION 3 --- + // This is the new validation logic. + // If the classification is NOT "None", we check if Optional Photo 1 and its remark have been provided. + if (!_isNoneClassificationSelected) { + if (widget.data.optionalImage1 == null || _remark1Controller.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Optional Photo 1 and its remark are mandatory for this classification.'), + backgroundColor: Colors.red, + ), + ); + return; + } + } + // --- END: MODIFICATION 3 --- + + // This is the existing validation you already had, which is still correct. if (widget.data.optionalImage1 != null && _remark1Controller.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('A remark is required for Optional Photo 1.'), backgroundColor: Colors.red), @@ -237,7 +236,6 @@ class _TarballSamplingStep2State extends State { ); return; } - // --- END: MODIFICATION 5 --- widget.data.optionalRemark1 = _remark1Controller.text; widget.data.optionalRemark2 = _remark2Controller.text; @@ -290,6 +288,20 @@ class _TarballSamplingStep2State extends State { _selectedClassification = value; widget.data.selectedClassification = value; widget.data.classificationId = value?['classification_id']; + + // --- START: MODIFICATION 4 --- + // Update the state variable whenever the dropdown changes. + // This will drive the conditional UI and validation. + _isNoneClassificationSelected = widget.data.classificationId == 1; + + // If user switches back to 'None', clear out old optional data + if (_isNoneClassificationSelected) { + widget.data.optionalImage1 = null; + widget.data.optionalRemark1 = null; + _remark1Controller.clear(); + // You might want to clear others too if needed + } + // --- END: MODIFICATION 4 --- }); }, validator: (value) => value == null ? 'Classification is required' : null, @@ -302,15 +314,28 @@ class _TarballSamplingStep2State extends State { _buildImagePicker('Left Side Coastal View', 'LEFTSIDECOASTALVIEW', widget.data.leftCoastalViewImage, (file) => widget.data.leftCoastalViewImage = file, isRequired: true), _buildImagePicker('Right Side Coastal View', 'RIGHTSIDECOASTALVIEW', widget.data.rightCoastalViewImage, (file) => widget.data.rightCoastalViewImage = file, isRequired: true), _buildImagePicker('Drawing Vertical Lines', 'VERTICALLINES', widget.data.verticalLinesImage, (file) => widget.data.verticalLinesImage = file, isRequired: true), - _buildImagePicker('Drawing Horizontal Line', 'HORIZONTALLINE', widget.data.horizontalLineImage, (file) => widget.data.horizontalLineImage = file, isRequired: true), + _buildImagePicker('Drawing Horizontal Lines (Racking)', 'HORIZONTALLINE', widget.data.horizontalLineImage, (file) => widget.data.horizontalLineImage = file, isRequired: true), - const SizedBox(height: 24), - Text("Optional Photos & Remarks", style: Theme.of(context).textTheme.titleLarge), - const SizedBox(height: 8), - _buildImagePicker('Optional Photo 1', 'OPTIONAL1', widget.data.optionalImage1, (file) => widget.data.optionalImage1 = file, remarkController: _remark1Controller), - _buildImagePicker('Optional Photo 2', 'OPTIONAL2', widget.data.optionalImage2, (file) => widget.data.optionalImage2 = file, remarkController: _remark2Controller), - _buildImagePicker('Optional Photo 3', 'OPTIONAL3', widget.data.optionalImage3, (file) => widget.data.optionalImage3 = file, remarkController: _remark3Controller), - _buildImagePicker('Optional Photo 4', 'OPTIONAL4', widget.data.optionalImage4, (file) => widget.data.optionalImage4 = file, remarkController: _remark4Controller), + // --- START: MODIFICATION 5 --- + // Wrap the entire "Optional Photos" section in a Visibility widget. + // It will only be visible if a classification has been selected AND it's not the "None" classification. + Visibility( + visible: _selectedClassification != null && !_isNoneClassificationSelected, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + Text("Optional Photos & Remarks", style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 8), + // For the first optional photo, we now mark it as required. + _buildImagePicker('Optional Photo 1', 'OPTIONAL1', widget.data.optionalImage1, (file) => widget.data.optionalImage1 = file, remarkController: _remark1Controller, isRequired: true), + _buildImagePicker('Optional Photo 2', 'OPTIONAL2', widget.data.optionalImage2, (file) => widget.data.optionalImage2 = file, remarkController: _remark2Controller), + _buildImagePicker('Optional Photo 3', 'OPTIONAL3', widget.data.optionalImage3, (file) => widget.data.optionalImage3 = file, remarkController: _remark3Controller), + _buildImagePicker('Optional Photo 4', 'OPTIONAL4', widget.data.optionalImage4, (file) => widget.data.optionalImage4 = file, remarkController: _remark4Controller), + ], + ), + ), + // --- END: MODIFICATION 5 --- const SizedBox(height: 32), ElevatedButton( @@ -357,11 +382,8 @@ class _TarballSamplingStep2State extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - // --- START: MODIFICATION 6 --- - // The `isRequired` parameter is no longer passed to `_setImage`. ElevatedButton.icon(onPressed: _isPickingImage ? null : () => _setImage(setImageCallback, ImageSource.camera, imageInfo), icon: const Icon(Icons.camera_alt), label: const Text("Camera")), ElevatedButton.icon(onPressed: _isPickingImage ? null : () => _setImage(setImageCallback, ImageSource.gallery, imageInfo), icon: const Icon(Icons.photo_library), label: const Text("Gallery")), - // --- END: MODIFICATION 6 --- ], ), if (remarkController != null) @@ -370,8 +392,8 @@ class _TarballSamplingStep2State extends State { child: TextFormField( controller: remarkController, decoration: InputDecoration( - labelText: 'Remarks for $title', - hintText: 'Add a remark...', // Changed hint text to be more direct + labelText: 'Remarks for $title' + (isRequired ? ' *' : ''), // Also indicate required status here + hintText: 'Add a remark...', border: const OutlineInputBorder(), ), ), diff --git a/lib/screens/marine/manual/tarball_sampling_step3_summary.dart b/lib/screens/marine/manual/tarball_sampling_step3_summary.dart index 7a0daa4..7afef30 100644 --- a/lib/screens/marine/manual/tarball_sampling_step3_summary.dart +++ b/lib/screens/marine/manual/tarball_sampling_step3_summary.dart @@ -6,9 +6,7 @@ import 'package:provider/provider.dart'; import 'package:environment_monitoring_app/auth_provider.dart'; import 'package:environment_monitoring_app/models/tarball_data.dart'; -// START CHANGE: Import the new dedicated service import 'package:environment_monitoring_app/services/marine_tarball_sampling_service.dart'; -// END CHANGE class TarballSamplingStep3Summary extends StatefulWidget { @@ -20,23 +18,16 @@ class TarballSamplingStep3Summary extends StatefulWidget { } class _TarballSamplingStep3SummaryState extends State { - // MODIFIED: The service instance is no longer created here. - // It will be fetched from the Provider where it's needed. - bool _isLoading = false; - // START CHANGE: The _submitForm method is now greatly simplified Future _submitForm() async { setState(() => _isLoading = true); final authProvider = Provider.of(context, listen: false); final appSettings = authProvider.appSettings; - // ADDED: Fetch the global service instance from Provider before using it. - // We use listen: false as this is a one-time action within a method. final tarballService = Provider.of(context, listen: false); - // Delegate the entire submission process to the new dedicated service final result = await tarballService.submitTarballSample( data: widget.data, appSettings: appSettings, @@ -56,7 +47,6 @@ class _TarballSamplingStep3SummaryState extends State route.isFirst); } - // END CHANGE @override Widget build(BuildContext context) { @@ -73,7 +63,7 @@ class _TarballSamplingStep3SummaryState extends State { // --- END MODIFICATION --- - final List _weatherOptions = ['Clear', 'Rainy', 'Cloudy']; + final List _weatherOptions = ['Clear', 'Cloudy', 'Drizzle', 'Rainy', 'Windy']; final List _tideOptions = ['High', 'Low', 'Mid']; final List _seaConditionOptions = ['Calm', 'Moderate Wave', 'High Wave']; diff --git a/lib/screens/marine/manual/widgets/in_situ_step_4_summary.dart b/lib/screens/marine/manual/widgets/in_situ_step_4_summary.dart index cd91d73..fa1bf4a 100644 --- a/lib/screens/marine/manual/widgets/in_situ_step_4_summary.dart +++ b/lib/screens/marine/manual/widgets/in_situ_step_4_summary.dart @@ -387,7 +387,7 @@ class _InSituStep4SummaryState extends State { const Divider(height: 20), _buildDetailRow("State:", widget.data.selectedStateName), _buildDetailRow("Category:", widget.data.selectedCategoryName), - _buildDetailRow("Station Code:", + _buildDetailRow("Station ID:", widget.data.selectedStation?['man_station_code']?.toString()), _buildDetailRow("Station Name:", widget.data.selectedStation?['man_station_name']?.toString()), diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index c581a98..f66ac24 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -748,7 +748,7 @@ class _SettingsScreenState extends State { ListTile( leading: const Icon(Icons.info_outline), title: const Text('App Version'), - subtitle: const Text('MMS Version 3.4.01'), + subtitle: const Text('MMS Version 3.5.01'), dense: true, ), ListTile(