repair marine manual and tarball screen
This commit is contained in:
parent
dff653883a
commit
821bb89ac4
@ -29,7 +29,7 @@ class _HomePageState extends State<HomePage> {
|
||||
});
|
||||
},
|
||||
),
|
||||
title: const Text("MMS Version 3.4.01"),
|
||||
title: const Text("MMS Version 3.5.01"),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person),
|
||||
|
||||
@ -25,9 +25,14 @@ class _TarballSamplingStep2State extends State<TarballSamplingStep2> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isPickingImage = false;
|
||||
|
||||
// This will hold the user's selection in the UI
|
||||
Map<String, dynamic>? _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<TarballSamplingStep2> {
|
||||
_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<AuthProvider>(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<TarballSamplingStep2> {
|
||||
);
|
||||
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<TarballSamplingStep2> {
|
||||
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<TarballSamplingStep2> {
|
||||
);
|
||||
}
|
||||
|
||||
// --- START: MODIFICATION 2 ---
|
||||
// The `isRequired` parameter has been removed. The orientation check will now
|
||||
// apply to every image processed by this function.
|
||||
Future<File?> _pickAndProcessImage(ImageSource source, String imageInfo) async {
|
||||
// --- END: MODIFICATION 2 ---
|
||||
if (_isPickingImage) return null;
|
||||
setState(() => _isPickingImage = true);
|
||||
|
||||
@ -124,15 +117,11 @@ class _TarballSamplingStep2State extends State<TarballSamplingStep2> {
|
||||
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<TarballSamplingStep2> {
|
||||
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<TarballSamplingStep2> {
|
||||
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<TarballSamplingStep2> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// --- END: MODIFICATION 5 ---
|
||||
|
||||
widget.data.optionalRemark1 = _remark1Controller.text;
|
||||
widget.data.optionalRemark2 = _remark2Controller.text;
|
||||
@ -290,6 +288,20 @@ class _TarballSamplingStep2State extends State<TarballSamplingStep2> {
|
||||
_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<TarballSamplingStep2> {
|
||||
_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<TarballSamplingStep2> {
|
||||
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<TarballSamplingStep2> {
|
||||
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(),
|
||||
),
|
||||
),
|
||||
|
||||
@ -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<TarballSamplingStep3Summary> {
|
||||
// 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<void> _submitForm() async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
final authProvider = Provider.of<AuthProvider>(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<MarineTarballSamplingService>(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<TarballSamplingStep3Summar
|
||||
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
// END CHANGE
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -73,7 +63,7 @@ class _TarballSamplingStep3SummaryState extends State<TarballSamplingStep3Summar
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildSectionCard(
|
||||
"Sampling Details",
|
||||
"Sampling Information",
|
||||
[
|
||||
_buildDetailRow("1st Sampler:", widget.data.firstSampler),
|
||||
_buildDetailRow("2nd Sampler:", widget.data.secondSampler?['first_name']?.toString()),
|
||||
@ -87,7 +77,7 @@ class _TarballSamplingStep3SummaryState extends State<TarballSamplingStep3Summar
|
||||
[
|
||||
_buildDetailRow("State:", widget.data.selectedStateName),
|
||||
_buildDetailRow("Category:", widget.data.selectedCategoryName),
|
||||
_buildDetailRow("Station Code:", widget.data.selectedStation?['tbl_station_code']?.toString()),
|
||||
_buildDetailRow("Station ID:", widget.data.selectedStation?['tbl_station_code']?.toString()),
|
||||
_buildDetailRow("Station Name:", widget.data.selectedStation?['tbl_station_name']?.toString()),
|
||||
_buildDetailRow("Station Latitude:", widget.data.stationLatitude),
|
||||
_buildDetailRow("Station Longitude:", widget.data.stationLongitude),
|
||||
@ -132,13 +122,26 @@ class _TarballSamplingStep3SummaryState extends State<TarballSamplingStep3Summar
|
||||
_buildImageCard("Right Side Coastal View", widget.data.rightCoastalViewImage),
|
||||
_buildImageCard("Drawing Vertical Lines", widget.data.verticalLinesImage),
|
||||
_buildImageCard("Drawing Horizontal Line", widget.data.horizontalLineImage),
|
||||
const Divider(height: 24),
|
||||
Text("Optional Photos", style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
_buildImageCard("Optional Photo 1", widget.data.optionalImage1, remark: widget.data.optionalRemark1),
|
||||
_buildImageCard("Optional Photo 2", widget.data.optionalImage2, remark: widget.data.optionalRemark2),
|
||||
_buildImageCard("Optional Photo 3", widget.data.optionalImage3, remark: widget.data.optionalRemark3),
|
||||
_buildImageCard("Optional Photo 4", widget.data.optionalImage4, remark: widget.data.optionalRemark4),
|
||||
|
||||
// --- START: MODIFICATION ---
|
||||
// Wrapped the optional photos section in a Visibility widget.
|
||||
// It will only be shown if the classification ID is not 1 (i.e., not "None").
|
||||
Visibility(
|
||||
visible: widget.data.classificationId != 1,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Divider(height: 24),
|
||||
Text("Optional Photos", style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
_buildImageCard("Optional Photo 1", widget.data.optionalImage1, remark: widget.data.optionalRemark1),
|
||||
_buildImageCard("Optional Photo 2", widget.data.optionalImage2, remark: widget.data.optionalRemark2),
|
||||
_buildImageCard("Optional Photo 3", widget.data.optionalImage3, remark: widget.data.optionalRemark3),
|
||||
_buildImageCard("Optional Photo 4", widget.data.optionalImage4, remark: widget.data.optionalRemark4),
|
||||
],
|
||||
),
|
||||
),
|
||||
// --- END: MODIFICATION ---
|
||||
],
|
||||
),
|
||||
|
||||
@ -216,6 +219,11 @@ class _TarballSamplingStep3SummaryState extends State<TarballSamplingStep3Summar
|
||||
}
|
||||
|
||||
Widget _buildImageCard(String title, File? image, {String? remark}) {
|
||||
// Only build the card if there is an image or a remark to show.
|
||||
if (image == null && (remark == null || remark.isEmpty)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
@ -242,7 +250,7 @@ class _TarballSamplingStep3SummaryState extends State<TarballSamplingStep3Summar
|
||||
if (remark != null && remark.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text('Remark: $remark', style: const TextStyle(fontStyle: FontStyle.italic, color: Colors.black54)),
|
||||
child: Text('Remark: $remark', style: const TextStyle(fontStyle: FontStyle.italic, color: Colors.grey)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -34,7 +34,7 @@ class _InSituStep2SiteInfoState extends State<InSituStep2SiteInfo> {
|
||||
// --- END MODIFICATION ---
|
||||
|
||||
|
||||
final List<String> _weatherOptions = ['Clear', 'Rainy', 'Cloudy'];
|
||||
final List<String> _weatherOptions = ['Clear', 'Cloudy', 'Drizzle', 'Rainy', 'Windy'];
|
||||
final List<String> _tideOptions = ['High', 'Low', 'Mid'];
|
||||
final List<String> _seaConditionOptions = ['Calm', 'Moderate Wave', 'High Wave'];
|
||||
|
||||
|
||||
@ -387,7 +387,7 @@ class _InSituStep4SummaryState extends State<InSituStep4Summary> {
|
||||
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()),
|
||||
|
||||
@ -748,7 +748,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
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(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user