repair marine manual and tarball screen

This commit is contained in:
ALim Aidrus 2025-10-16 21:52:01 +08:00
parent dff653883a
commit 821bb89ac4
6 changed files with 96 additions and 66 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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