diff --git a/lib/home_page.dart b/lib/home_page.dart index 1c98a07..541d81e 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.2.01"), + title: const Text("MMS Version 3.4.01"), actions: [ IconButton( icon: const Icon(Icons.person), diff --git a/lib/main.dart b/lib/main.dart index 2b2c9ef..2967acf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -72,13 +72,13 @@ import 'package:environment_monitoring_app/screens/river/investigative/report.da // Marine Screens import 'package:environment_monitoring_app/screens/marine/manual/info_centre_document.dart' as marineManualInfoCentreDocument; -import 'package:environment_monitoring_app/screens/marine/manual/pre_sampling.dart' as marineManualPreSampling; +import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_pre_sampling.dart' as marineManualPreSampling; import 'package:environment_monitoring_app/screens/marine/manual/in_situ_sampling.dart' as marineManualInSituSampling; import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_report.dart' as marineManualReport; import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_npe_report.dart' as marineManualNPEReport; -import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_pre_departure_checklist_screen.dart'; -import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_sonde_calibration_screen.dart'; -import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_equipment_maintenance_screen.dart'; +import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_pre_departure_checklist_screen.dart' as marineManualPreDepartureChecklist; +import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_sonde_calibration_screen.dart' as marineManualSondeCalibration; +import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_equipment_maintenance_screen.dart' as marineManualEquipmentMaintenance; import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_data_status_log.dart' as marineManualDataStatusLog; import 'package:environment_monitoring_app/screens/marine/manual/marine_image_request.dart' as marineManualImageRequest; import 'package:environment_monitoring_app/screens/marine/continuous/marine_continuous_info_centre_document.dart'; @@ -326,14 +326,14 @@ class _RootAppState extends State { // Marine Manual '/marine/manual/info': (context) => marineManualInfoCentreDocument.MarineInfoCentreDocument(), - '/marine/manual/pre-sampling': (context) => marineManualPreSampling.MarinePreSampling(), + '/marine/manual/pre-sampling': (context) => marineManualPreSampling.MarineManualPreSampling(), '/marine/manual/in-situ': (context) => marineManualInSituSampling.MarineInSituSampling(), '/marine/manual/tarball': (context) => const TarballSamplingStep1(), '/marine/manual/report': (context) => const marineManualReport.MarineManualReportHomePage(), '/marine/manual/report/npe': (context) => const marineManualNPEReport.MarineManualNPEReport(), - '/marine/manual/report/pre-departure': (context) => const MarineManualPreDepartureChecklistScreen(), - '/marine/manual/report/calibration': (context) => const MarineManualSondeCalibrationScreen(), - '/marine/manual/report/maintenance': (context) => const MarineManualEquipmentMaintenanceScreen(), + '/marine/manual/report/pre-departure': (context) => const marineManualPreDepartureChecklist.MarineManualPreDepartureChecklistScreen(), + '/marine/manual/report/calibration': (context) => const marineManualSondeCalibration.MarineManualSondeCalibrationScreen(), + '/marine/manual/report/maintenance': (context) => const marineManualEquipmentMaintenance.MarineManualEquipmentMaintenanceScreen(), //'/marine/manual/data-log': (context) => marineManualDataStatusLog.MarineManualDataStatusLog(), // This is handled in onGenerateRoute '/marine/manual/image-request': (context) => const marineManualImageRequest.MarineImageRequestScreen(), diff --git a/lib/models/in_situ_sampling_data.dart b/lib/models/in_situ_sampling_data.dart index 94ae359..0bc6252 100644 --- a/lib/models/in_situ_sampling_data.dart +++ b/lib/models/in_situ_sampling_data.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'dart:convert'; // Added for jsonEncode +import 'package:environment_monitoring_app/models/marine_manual_npe_report_data.dart'; + /// A data model class to hold all information for the multi-step /// In-Situ Sampling form. class InSituSamplingData { @@ -103,6 +105,61 @@ class InSituSamplingData { this.samplingTime, }); + /// Creates a pre-populated NPE Report object from the current In-Situ data. + MarineManualNpeReportData toNpeReportData() { + final npeData = MarineManualNpeReportData(); + + // Transfer Reporter & Event Info + npeData.firstSamplerName = firstSamplerName; + npeData.firstSamplerUserId = firstSamplerUserId; + npeData.eventDate = samplingDate; + npeData.eventTime = samplingTime; + + // Transfer Location Info + npeData.selectedStation = selectedStation; + npeData.latitude = currentLatitude; + npeData.longitude = currentLongitude; + + // Transfer In-Situ Measurements relevant to NPE + npeData.oxygenSaturation = oxygenSaturation; + npeData.electricalConductivity = electricalConductivity; + npeData.oxygenConcentration = oxygenConcentration; + npeData.turbidity = turbidity; + npeData.ph = ph; + npeData.temperature = temperature; + + // Pre-populate possible source with event remarks as a starting point for the user + npeData.possibleSource = eventRemarks; + + // Pre-populate some common observations based on data + if ((turbidity ?? 0) > 50) { // Example threshold, adjust as needed + npeData.fieldObservations['Silt plume'] = true; + } + if ((oxygenConcentration ?? 999) < 4) { // Example threshold for low oxygen + npeData.fieldObservations['Foul smell'] = true; + } + + // Transfer up to 4 available images + final availableImages = [ + leftLandViewImage, + rightLandViewImage, + waterFillingImage, + seawaterColorImage, + phPaperImage, + optionalImage1, + optionalImage2, + optionalImage3, + optionalImage4, + ].where((img) => img != null).cast().toList(); + + if (availableImages.isNotEmpty) npeData.image1 = availableImages[0]; + if (availableImages.length > 1) npeData.image2 = availableImages[1]; + if (availableImages.length > 2) npeData.image3 = availableImages[2]; + if (availableImages.length > 3) npeData.image4 = availableImages[3]; + + return npeData; + } + /// Creates an InSituSamplingData object from a JSON map. factory InSituSamplingData.fromJson(Map json) { double? doubleFromJson(dynamic value) { @@ -181,7 +238,7 @@ class InSituSamplingData { data.optionalImage4 = fileFromPath(json['man_optional_photo_04']); - // --- START: Deserialization for NPE Fields --- + // --- Deserialization for NPE Fields --- if (json['npe_field_observations'] is Map) { data.npeFieldObservations = Map.from(json['npe_field_observations']); } @@ -193,17 +250,10 @@ class InSituSamplingData { data.npeImage2 = fileFromPath(json['npe_image_2']); data.npeImage3 = fileFromPath(json['npe_image_3']); data.npeImage4 = fileFromPath(json['npe_image_4']); - // --- END: Deserialization for NPE Fields --- return data; } - // ... (generateTelegramAlertMessage method remains unchanged) ... - - // ... (toApiFormData method remains unchanged) ... - - // ... (toApiImageFiles method remains unchanged) ... - /// Creates a single JSON object with all submission data for offline storage. Map toDbJson() { return { @@ -212,7 +262,6 @@ class InSituSamplingData { 'secondSampler': secondSampler, 'sampling_date': samplingDate, 'sampling_time': samplingTime, - // ... (all other existing fields) 'sampling_type': samplingType, 'sample_id_code': sampleIdCode, 'selected_state_name': selectedStateName, @@ -249,18 +298,12 @@ class InSituSamplingData { 'submission_status': submissionStatus, 'submission_message': submissionMessage, 'report_id': reportId, - - // --- START: Serialization for NPE Fields --- 'npe_field_observations': npeFieldObservations, 'npe_others_observation_remark': npeOthersObservationRemark, 'npe_possible_source': npePossibleSource, - // Note: Image file paths are handled separately by the LocalStorageService - // and are not part of this JSON object directly. - // --- END: Serialization for NPE Fields --- }; } - // --- Methods from the original file --- String generateTelegramAlertMessage({required bool isDataOnly}) { final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)"; final stationName = selectedStation?['man_station_name'] ?? 'N/A'; diff --git a/lib/screens/marine/manual/in_situ_sampling.dart b/lib/screens/marine/manual/in_situ_sampling.dart index 06d9dfe..76d096f 100644 --- a/lib/screens/marine/manual/in_situ_sampling.dart +++ b/lib/screens/marine/manual/in_situ_sampling.dart @@ -45,12 +45,6 @@ class _MarineInSituSamplingState extends State { @override void dispose() { _pageController.dispose(); - // START FIX: Do not dispose the service here. - // The service is managed by the Provider at a higher level in the app. Disposing it - // here can cause the "deactivated widget's ancestor" error if other widgets - // are still listening to it during screen transitions. - // _samplingService.dispose(); - // END FIX super.dispose(); } @@ -72,7 +66,9 @@ class _MarineInSituSamplingState extends State { } } - Future _submitForm() async { + /// This function contains the core logic for submitting data and returns the result. + /// UI actions like SnackBars and navigation are handled by the caller (summary screen). + Future> _submitFormLogic() async { setState(() => _isLoading = true); final authProvider = Provider.of(context, listen: false); @@ -85,17 +81,11 @@ class _MarineInSituSamplingState extends State { authProvider: authProvider, ); - if (!mounted) return; + if (mounted) { + setState(() => _isLoading = false); + } - setState(() => _isLoading = false); - - final message = result['message'] ?? 'An unknown error occurred.'; - final color = (result['success'] == true) ? Colors.green : Colors.red; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: color, duration: const Duration(seconds: 4)), - ); - - Navigator.of(context).popUntil((route) => route.isFirst); + return result; } @@ -125,7 +115,7 @@ class _MarineInSituSamplingState extends State { InSituStep1SamplingInfo(data: _data, onNext: _nextPage), InSituStep2SiteInfo(data: _data, onNext: _nextPage), InSituStep3DataCapture(data: _data, onNext: _nextPage), - InSituStep4Summary(data: _data, onSubmit: _submitForm, isLoading: _isLoading), + InSituStep4Summary(data: _data, onSubmit: _submitFormLogic, isLoading: _isLoading), ], ), ), diff --git a/lib/screens/marine/manual/marine_manual_pre_sampling.dart b/lib/screens/marine/manual/marine_manual_pre_sampling.dart new file mode 100644 index 0000000..954676e --- /dev/null +++ b/lib/screens/marine/manual/marine_manual_pre_sampling.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_report.dart'; + +class MarineManualPreSampling extends StatelessWidget { + const MarineManualPreSampling({super.key}); + + // This list includes the pre-sampling items, excluding the NPE report. + // The routes point to the same screens used by the report module. + final List _preSamplingItems = const [ + ReportItem( + icon: Icons.biotech_rounded, + label: "Sonde Calibration", + formCode: "F-MM02", + route: '/marine/manual/report/calibration', + ), + ReportItem( + icon: Icons.checklist_rtl_rounded, + label: "Pre-Departure & Safety Checklist", + formCode: "F-MM03", + route: '/marine/manual/report/pre-departure', + ), + ReportItem( + icon: Icons.build_circle_outlined, + label: "Equipment Maintenance", + formCode: "F-MM01", + route: '/marine/manual/report/maintenance', + ), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Marine Pre-Sampling"), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Select a Pre-Sampling Form", + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 16.0, + mainAxisSpacing: 16.0, + childAspectRatio: 1.2, // Adjusted for better text fit + ), + itemCount: _preSamplingItems.length, + itemBuilder: (context, index) { + final item = _preSamplingItems[index]; + return _buildReportCard(context, item); + }, + ), + ], + ), + ), + ); + } + + // This widget is copied from MarineManualReportHomePage for visual consistency. + Widget _buildReportCard(BuildContext context, ReportItem item) { + return InkWell( + onTap: () { + Navigator.pushNamed(context, item.route); + }, + borderRadius: BorderRadius.circular(12), + child: Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: Colors.white24, width: 1), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(item.icon, size: 40, color: Theme.of(context).colorScheme.secondary), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + item.label, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(height: 4), + Text( + item.formCode, // Displaying the form code + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[400]), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/marine/manual/pre_sampling.dart b/lib/screens/marine/manual/pre_sampling.dart deleted file mode 100644 index e11552c..0000000 --- a/lib/screens/marine/manual/pre_sampling.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; - -class MarinePreSampling extends StatefulWidget { - @override - State createState() => _MarinePreSamplingState(); -} - -class _MarinePreSamplingState extends State { - final _formKey = GlobalKey(); - String site = ''; - String weather = ''; - String tide = ''; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text("Marine Pre-Sampling")), - body: Padding( - padding: const EdgeInsets.all(24), - child: Form( - key: _formKey, - child: ListView( - children: [ - Text("Enter Pre-Sampling Conditions", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - SizedBox(height: 24), - TextFormField( - decoration: InputDecoration(labelText: "Site"), - onChanged: (val) => site = val, - validator: (val) => val == null || val.isEmpty ? "Required" : null, - ), - SizedBox(height: 16), - TextFormField( - decoration: InputDecoration(labelText: "Weather"), - onChanged: (val) => weather = val, - validator: (val) => val == null || val.isEmpty ? "Required" : null, - ), - SizedBox(height: 16), - TextFormField( - decoration: InputDecoration(labelText: "Tide Condition"), - onChanged: (val) => tide = val, - validator: (val) => val == null || val.isEmpty ? "Required" : null, - ), - SizedBox(height: 24), - ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Pre-sampling data submitted")), - ); - } - }, - child: Text("Submit"), - ), - ], - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/marine/manual/tarball_sampling_step2.dart b/lib/screens/marine/manual/tarball_sampling_step2.dart index 4e9de08..3925863 100644 --- a/lib/screens/marine/manual/tarball_sampling_step2.dart +++ b/lib/screens/marine/manual/tarball_sampling_step2.dart @@ -86,7 +86,10 @@ class _TarballSamplingStep2State extends State { builder: (BuildContext context) { return AlertDialog( title: const Text("Incorrect Image Orientation"), - content: const Text("Required photos must be taken in a horizontal (landscape) 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"), @@ -98,7 +101,11 @@ class _TarballSamplingStep2State extends State { ); } - Future _pickAndProcessImage(ImageSource source, String imageInfo, {required bool isRequired}) async { + // --- 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); @@ -117,11 +124,15 @@ class _TarballSamplingStep2State extends State { return null; } - if (isRequired && originalImage.height > originalImage.width) { + // --- 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; @@ -155,19 +166,18 @@ class _TarballSamplingStep2State extends State { final tempDir = await getTemporaryDirectory(); final filePath = path.join(tempDir.path, newFileName); - // --- START: MODIFICATION TO FIX RACE CONDITION --- - // Changed from asynchronous `writeAsBytes` to synchronous `writeAsBytesSync`. - // This guarantees the file is fully written to disk before the function returns, - // preventing a 0-byte file from being copied during a fast offline save. final File processedFile = File(filePath)..writeAsBytesSync(img.encodeJpg(originalImage)); - // --- END: MODIFICATION TO FIX RACE CONDITION --- setState(() => _isPickingImage = false); return processedFile; } - void _setImage(Function(File?) setImageCallback, ImageSource source, String imageInfo, {required bool isRequired}) async { - final file = await _pickAndProcessImage(source, imageInfo, isRequired: isRequired); + // --- 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); @@ -200,6 +210,35 @@ 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. + 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), + ); + return; + } + if (widget.data.optionalImage2 != null && _remark2Controller.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('A remark is required for Optional Photo 2.'), backgroundColor: Colors.red), + ); + return; + } + if (widget.data.optionalImage3 != null && _remark3Controller.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('A remark is required for Optional Photo 3.'), backgroundColor: Colors.red), + ); + return; + } + if (widget.data.optionalImage4 != null && _remark4Controller.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('A remark is required for Optional Photo 4.'), backgroundColor: Colors.red), + ); + return; + } + // --- END: MODIFICATION 5 --- + widget.data.optionalRemark1 = _remark1Controller.text; widget.data.optionalRemark2 = _remark2Controller.text; widget.data.optionalRemark3 = _remark3Controller.text; @@ -224,15 +263,13 @@ class _TarballSamplingStep2State extends State { Text("On-Site Information", style: Theme.of(context).textTheme.headlineSmall), const SizedBox(height: 24), - // This dropdown now correctly consumes data from AuthProvider Consumer( builder: (context, auth, child) { final classifications = auth.tarballClassifications; - // The dropdown is enabled only when the classification list is available from the local cache. final bool isEnabled = classifications != null; return DropdownSearch>( - items: classifications ?? [], // Use local data from provider + items: classifications ?? [], selectedItem: _selectedClassification, enabled: isEnabled, itemAsString: (item) => item['classification_name'] as String, @@ -251,7 +288,6 @@ class _TarballSamplingStep2State extends State { onChanged: (value) { setState(() { _selectedClassification = value; - // NECESSARY CHANGE: Save both the full object and the ID to the data model. widget.data.selectedClassification = value; widget.data.classificationId = value?['classification_id']; }); @@ -321,8 +357,11 @@ class _TarballSamplingStep2State extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - ElevatedButton.icon(onPressed: _isPickingImage ? null : () => _setImage(setImageCallback, ImageSource.camera, imageInfo, isRequired: isRequired), icon: const Icon(Icons.camera_alt), label: const Text("Camera")), - ElevatedButton.icon(onPressed: _isPickingImage ? null : () => _setImage(setImageCallback, ImageSource.gallery, imageInfo, isRequired: isRequired), icon: const Icon(Icons.photo_library), label: const Text("Gallery")), + // --- 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) @@ -332,7 +371,7 @@ class _TarballSamplingStep2State extends State { controller: remarkController, decoration: InputDecoration( labelText: 'Remarks for $title', - hintText: 'Add an optional remark...', + hintText: 'Add a remark...', // Changed hint text to be more direct border: const OutlineInputBorder(), ), ), 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 9dbc4dd..cd91d73 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 @@ -6,10 +6,12 @@ import 'package:provider/provider.dart'; import '../../../../auth_provider.dart'; import '../../../../models/in_situ_sampling_data.dart'; +import '../../../../models/marine_manual_npe_report_data.dart'; -class InSituStep4Summary extends StatelessWidget { +class InSituStep4Summary extends StatefulWidget { final InSituSamplingData data; - final VoidCallback onSubmit; + final Future> Function() + onSubmit; // Expects a function that returns the submission result final bool isLoading; const InSituStep4Summary({ @@ -19,11 +21,14 @@ class InSituStep4Summary extends StatelessWidget { required this.isLoading, }); - // --- START: MODIFICATION FOR HIGHLIGHTING --- - // Added helper logic to re-validate parameters on the summary screen. + @override + State createState() => _InSituStep4SummaryState(); +} - /// Maps the app's internal parameter keys to the names used in the database. - static const Map _parameterKeyToLimitName = const { +class _InSituStep4SummaryState extends State { + bool _isHandlingSubmit = false; + + static const Map _parameterKeyToLimitName = { 'oxygenConcentration': 'Oxygen Conc', 'oxygenSaturation': 'Oxygen Sat', 'ph': 'pH', @@ -36,18 +41,23 @@ class InSituStep4Summary extends StatelessWidget { 'batteryVoltage': 'Battery', }; - /// Re-validates the final parameters against the defined limits. Set _getOutOfBoundsKeys(BuildContext context) { final authProvider = Provider.of(context, listen: false); final marineLimits = authProvider.marineParameterLimits ?? []; final Set invalidKeys = {}; - final int? stationId = data.selectedStation?['station_id']; + final int? stationId = widget.data.selectedStation?['station_id']; 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, - 'tss': data.tss, 'batteryVoltage': data.batteryVoltage, + 'oxygenConcentration': widget.data.oxygenConcentration, + 'oxygenSaturation': widget.data.oxygenSaturation, + 'ph': widget.data.ph, + 'salinity': widget.data.salinity, + 'electricalConductivity': widget.data.electricalConductivity, + 'temperature': widget.data.temperature, + 'tds': widget.data.tds, + 'turbidity': widget.data.turbidity, + 'tss': widget.data.tss, + 'batteryVoltage': widget.data.batteryVoltage, }; double? parseLimitValue(dynamic value) { @@ -63,22 +73,23 @@ class InSituStep4Summary extends StatelessWidget { final limitName = _parameterKeyToLimitName[key]; if (limitName == null) return; - // START MODIFICATION: Only check for station-specific limits Map limitData = {}; if (stationId != null) { limitData = marineLimits.firstWhere( - (l) => l['param_parameter_list'] == limitName && l['station_id'] == stationId, + (l) => + l['param_parameter_list'] == limitName && + l['station_id'] == stationId, orElse: () => {}, ); } - // END MODIFICATION 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)) { + if ((lowerLimit != null && value < lowerLimit) || + (upperLimit != null && value > upperLimit)) { invalidKeys.add(key); } } @@ -86,14 +97,272 @@ class InSituStep4Summary extends StatelessWidget { return invalidKeys; } - // --- END: MODIFICATION FOR HIGHLIGHTING --- + + /// Checks captured data against NPE limits and returns detailed information for the dialog. + List> _getNpeTriggeredParameters(BuildContext context) { + final authProvider = Provider.of(context, listen: false); + final npeLimits = authProvider.npeParameterLimits ?? []; + if (npeLimits.isEmpty) return []; + + final List> triggeredParams = []; + + final readings = { + 'oxygenConcentration': widget.data.oxygenConcentration, + 'oxygenSaturation': widget.data.oxygenSaturation, + 'ph': widget.data.ph, + 'salinity': widget.data.salinity, + 'electricalConductivity': widget.data.electricalConductivity, + 'temperature': widget.data.temperature, + 'tds': widget.data.tds, + 'turbidity': widget.data.turbidity, + 'tss': widget.data.tss, + }; + + 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 = npeLimits.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']); + bool isHit = false; + + if (lowerLimit != null && upperLimit != null) { + if (value >= lowerLimit && value <= upperLimit) isHit = true; + } else if (lowerLimit != null && upperLimit == null) { + if (value >= lowerLimit) isHit = true; + } else if (upperLimit != null && lowerLimit == null) { + if (value <= upperLimit) isHit = true; + } + + if (isHit) { + triggeredParams.add({ + 'label': limitName, + 'value': value, + 'lower_limit': lowerLimit, + 'upper_limit': upperLimit, + }); + } + } + }); + return triggeredParams; + } + + /// Displays the enhanced NPE warning dialog with a highlighted table. + Future _showNpeDialog( + BuildContext context, List> triggeredParams) async { + final isDarkTheme = Theme.of(context).brightness == Brightness.dark; + + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Text("NPE Parameter Limit Detected"), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'The following parameters have fallen under the Non-Permissible Event (NPE) limit:'), + const SizedBox(height: 16), + Table( + columnWidths: const { + 0: FlexColumnWidth(2), + 1: FlexColumnWidth(2.5), + 2: FlexColumnWidth(1.5), + }, + border: TableBorder( + horizontalInside: BorderSide( + width: 0.5, + color: isDarkTheme + ? Colors.grey.shade700 + : Colors.grey.shade300), + verticalInside: BorderSide( + width: 0.5, + color: isDarkTheme + ? Colors.grey.shade700 + : Colors.grey.shade300), + top: BorderSide( + width: 1, + color: isDarkTheme + ? Colors.grey.shade600 + : Colors.grey.shade400), + bottom: BorderSide( + width: 1, + color: isDarkTheme + ? Colors.grey.shade600 + : Colors.grey.shade400), + ), + children: [ + TableRow( + decoration: BoxDecoration( + color: isDarkTheme + ? Colors.grey.shade800 + : Colors.grey.shade200), + children: [ + Padding( + padding: const EdgeInsets.all(6.0), + child: Text('Parameter', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .textTheme + .titleSmall + ?.color))), + Padding( + padding: const EdgeInsets.all(6.0), + child: Text('NPE Range', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .textTheme + .titleSmall + ?.color))), + Padding( + padding: const EdgeInsets.all(6.0), + child: Text('Current', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .textTheme + .titleSmall + ?.color))), + ], + ), + ...triggeredParams.map((p) { + final lowerStr = + p['lower_limit']?.toStringAsFixed(5) ?? 'N/A'; + final upperStr = + p['upper_limit']?.toStringAsFixed(5) ?? 'N/A'; + String range; + if (lowerStr != 'N/A' && upperStr != 'N/A') { + range = '$lowerStr - $upperStr'; + } else if (lowerStr != 'N/A') { + range = '>= $lowerStr'; + } else { + range = '<= $upperStr'; + } + + return TableRow( + children: [ + Padding( + padding: const EdgeInsets.all(6.0), + child: Text(p['label'])), + Padding( + padding: const EdgeInsets.all(6.0), + child: Text(range)), + Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + p['value'].toStringAsFixed(5), + style: const TextStyle( + color: Colors.redAccent, + fontWeight: FontWeight.bold), + ), + ), + ], + ); + }).toList(), + ], + ), + const SizedBox(height: 16), + const Text('Please generate an NPE report.'), + ], + ), + ), + actions: [ + TextButton( + child: const Text("Continue"), + onPressed: () => Navigator.of(dialogContext).pop(false), + ), + ElevatedButton( + child: const Text("Open NPE Report"), + onPressed: () => Navigator.of(dialogContext).pop(true), + ), + ], + ); + }, + ); + } + + /// Handles the complete submission flow: NPE check, submission, and UI feedback/navigation. + Future _handleSubmit(BuildContext context) async { + if (_isHandlingSubmit || widget.isLoading) return; + + setState(() => _isHandlingSubmit = true); + + bool shouldOpenNpeReport = false; + bool proceedWithSubmission = false; + + try { + final npeParameters = _getNpeTriggeredParameters(context); + + if (npeParameters.isNotEmpty) { + final userChoice = await _showNpeDialog(context, npeParameters); + if (userChoice == true) { + shouldOpenNpeReport = true; + proceedWithSubmission = true; + } else if (userChoice == false) { + proceedWithSubmission = true; + } + // If userChoice is null (dialog dismissed), we do nothing. + } else { + // No NPE hit, proceed normally. + proceedWithSubmission = true; + } + + if (proceedWithSubmission) { + final result = await widget.onSubmit(); // This calls the logic in the parent + if (!mounted) return; + + final message = result['message'] ?? 'An unknown error occurred.'; + final color = (result['success'] == true) ? Colors.green : Colors.red; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: color, + duration: const Duration(seconds: 4)), + ); + + if (result['success'] == true) { + if (shouldOpenNpeReport) { + final npeData = widget.data.toNpeReportData(); + // This works offline because it's just passing local data + Navigator.of(context).pushNamed( + '/marine/manual/report/npe', + arguments: npeData, + ); + } else { + // Submission successful, and no NPE report needed, so go home. + Navigator.of(context).popUntil((route) => route.isFirst); + } + } + } + } finally { + if (mounted) { + setState(() => _isHandlingSubmit = false); + } + } + } @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), @@ -104,94 +373,164 @@ class InSituStep4Summary extends StatelessWidget { textAlign: TextAlign.center, ), const SizedBox(height: 16), - _buildSectionCard( context, "Sampling & Station Details", [ - _buildDetailRow("1st Sampler:", data.firstSamplerName), - _buildDetailRow("2nd Sampler:", data.secondSampler?['first_name']?.toString()), - _buildDetailRow("Sampling Date:", data.samplingDate), - _buildDetailRow("Sampling Time:", data.samplingTime), - _buildDetailRow("Sampling Type:", data.samplingType), - _buildDetailRow("Sample ID Code:", data.sampleIdCode), + _buildDetailRow("1st Sampler:", widget.data.firstSamplerName), + _buildDetailRow( + "2nd Sampler:", widget.data.secondSampler?['first_name']?.toString()), + _buildDetailRow("Sampling Date:", widget.data.samplingDate), + _buildDetailRow("Sampling Time:", widget.data.samplingTime), + _buildDetailRow("Sampling Type:", widget.data.samplingType), + _buildDetailRow("Sample ID Code:", widget.data.sampleIdCode), const Divider(height: 20), - _buildDetailRow("State:", data.selectedStateName), - _buildDetailRow("Category:", data.selectedCategoryName), - _buildDetailRow("Station Code:", data.selectedStation?['man_station_code']?.toString()), - _buildDetailRow("Station Name:", data.selectedStation?['man_station_name']?.toString()), - _buildDetailRow("Station Location:", "${data.stationLatitude}, ${data.stationLongitude}"), + _buildDetailRow("State:", widget.data.selectedStateName), + _buildDetailRow("Category:", widget.data.selectedCategoryName), + _buildDetailRow("Station Code:", + widget.data.selectedStation?['man_station_code']?.toString()), + _buildDetailRow("Station Name:", + widget.data.selectedStation?['man_station_name']?.toString()), + _buildDetailRow("Station Location:", + "${widget.data.stationLatitude}, ${widget.data.stationLongitude}"), ], ), - _buildSectionCard( context, "Location & On-Site Info", [ - _buildDetailRow("Current Location:", "${data.currentLatitude}, ${data.currentLongitude}"), - _buildDetailRow("Distance Difference:", data.distanceDifferenceInKm != null ? "${(data.distanceDifferenceInKm! * 1000).toStringAsFixed(0)} meters" : "N/A"), - if (data.distanceDifferenceRemarks != null && data.distanceDifferenceRemarks!.isNotEmpty) - _buildDetailRow("Distance Remarks:", data.distanceDifferenceRemarks), + _buildDetailRow("Current Location:", + "${widget.data.currentLatitude}, ${widget.data.currentLongitude}"), + _buildDetailRow( + "Distance Difference:", + widget.data.distanceDifferenceInKm != null + ? "${(widget.data.distanceDifferenceInKm! * 1000).toStringAsFixed(0)} meters" + : "N/A"), + if (widget.data.distanceDifferenceRemarks != null && + widget.data.distanceDifferenceRemarks!.isNotEmpty) + _buildDetailRow( + "Distance Remarks:", widget.data.distanceDifferenceRemarks), const Divider(height: 20), - _buildDetailRow("Weather:", data.weather), - _buildDetailRow("Tide Level:", data.tideLevel), - _buildDetailRow("Sea Condition:", data.seaCondition), - _buildDetailRow("Event Remarks:", data.eventRemarks), - _buildDetailRow("Lab Remarks:", data.labRemarks), + _buildDetailRow("Weather:", widget.data.weather), + _buildDetailRow("Tide Level:", widget.data.tideLevel), + _buildDetailRow("Sea Condition:", widget.data.seaCondition), + _buildDetailRow("Event Remarks:", widget.data.eventRemarks), + _buildDetailRow("Lab Remarks:", widget.data.labRemarks), ], ), - _buildSectionCard( context, "Attached Photos", [ - _buildImageCard("Left Side Land View", data.leftLandViewImage), - _buildImageCard("Right Side Land View", data.rightLandViewImage), - _buildImageCard("Filling Water into Bottle", data.waterFillingImage), - _buildImageCard("Seawater Color in Bottle", data.seawaterColorImage), - _buildImageCard("Examine Preservative (pH paper)", data.phPaperImage), + _buildImageCard("Left Side Land View", widget.data.leftLandViewImage), + _buildImageCard( + "Right Side Land View", widget.data.rightLandViewImage), + _buildImageCard( + "Filling Water into Bottle", widget.data.waterFillingImage), + _buildImageCard( + "Seawater Color in Bottle", widget.data.seawaterColorImage), + _buildImageCard( + "Examine Preservative (pH paper)", widget.data.phPaperImage), const Divider(height: 24), - Text("Optional Photos", style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)), + Text("Optional Photos", + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 8), - _buildImageCard("Optional Photo 1", data.optionalImage1, remark: data.optionalRemark1), - _buildImageCard("Optional Photo 2", data.optionalImage2, remark: data.optionalRemark2), - _buildImageCard("Optional Photo 3", data.optionalImage3, remark: data.optionalRemark3), - _buildImageCard("Optional Photo 4", data.optionalImage4, remark: data.optionalRemark4), + _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), ], ), - _buildSectionCard( context, "Captured Parameters", [ - _buildDetailRow("Sonde ID:", data.sondeId), - _buildDetailRow("Capture Time:", "${data.dataCaptureDate} ${data.dataCaptureTime}"), + _buildDetailRow("Sonde ID:", widget.data.sondeId), + _buildDetailRow("Capture Time:", + "${widget.data.dataCaptureDate} ${widget.data.dataCaptureTime}"), const Divider(height: 20), - // --- 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.filter_alt_outlined, label: "TSS", unit: "mg/L", value: data.tss, isOutOfBounds: outOfBoundsKeys.contains('tss')), - _buildParameterListItem(context, icon: Icons.battery_charging_full, label: "Battery", unit: "V", value: data.batteryVoltage, isOutOfBounds: outOfBoundsKeys.contains('batteryVoltage')), - // --- END: MODIFICATION --- + _buildParameterListItem(context, + icon: Icons.air, + label: "Oxygen Conc.", + unit: "mg/L", + value: widget.data.oxygenConcentration, + isOutOfBounds: + outOfBoundsKeys.contains('oxygenConcentration')), + _buildParameterListItem(context, + icon: Icons.percent, + label: "Oxygen Sat.", + unit: "%", + value: widget.data.oxygenSaturation, + isOutOfBounds: outOfBoundsKeys.contains('oxygenSaturation')), + _buildParameterListItem(context, + icon: Icons.science_outlined, + label: "pH", + unit: "", + value: widget.data.ph, + isOutOfBounds: outOfBoundsKeys.contains('ph')), + _buildParameterListItem(context, + icon: Icons.waves, + label: "Salinity", + unit: "ppt", + value: widget.data.salinity, + isOutOfBounds: outOfBoundsKeys.contains('salinity')), + _buildParameterListItem(context, + icon: Icons.flash_on, + label: "Conductivity", + unit: "µS/cm", + value: widget.data.electricalConductivity, + isOutOfBounds: + outOfBoundsKeys.contains('electricalConductivity')), + _buildParameterListItem(context, + icon: Icons.thermostat, + label: "Temperature", + unit: "°C", + value: widget.data.temperature, + isOutOfBounds: outOfBoundsKeys.contains('temperature')), + _buildParameterListItem(context, + icon: Icons.grain, + label: "TDS", + unit: "mg/L", + value: widget.data.tds, + isOutOfBounds: outOfBoundsKeys.contains('tds')), + _buildParameterListItem(context, + icon: Icons.opacity, + label: "Turbidity", + unit: "NTU", + value: widget.data.turbidity, + isOutOfBounds: outOfBoundsKeys.contains('turbidity')), + _buildParameterListItem(context, + icon: Icons.filter_alt_outlined, + label: "TSS", + unit: "mg/L", + value: widget.data.tss, + isOutOfBounds: outOfBoundsKeys.contains('tss')), + _buildParameterListItem(context, + icon: Icons.battery_charging_full, + label: "Battery", + unit: "V", + value: widget.data.batteryVoltage, + isOutOfBounds: outOfBoundsKeys.contains('batteryVoltage')), ], ), - const SizedBox(height: 24), - isLoading + (widget.isLoading || _isHandlingSubmit) ? const Center(child: CircularProgressIndicator()) : ElevatedButton.icon( - onPressed: onSubmit, + onPressed: () => _handleSubmit(context), icon: const Icon(Icons.cloud_upload), label: const Text('Confirm & Submit'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), - textStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + textStyle: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), ), ), const SizedBox(height: 16), @@ -199,8 +538,8 @@ class InSituStep4Summary extends StatelessWidget { ); } - /// A reusable widget to create a consistent section card. - Widget _buildSectionCard(BuildContext context, String title, List children) { + Widget _buildSectionCard( + BuildContext context, String title, List children) { return Card( margin: const EdgeInsets.symmetric(vertical: 8.0), elevation: 2, @@ -226,7 +565,6 @@ class InSituStep4Summary extends StatelessWidget { ); } - /// A reusable widget for a label-value row. Widget _buildDetailRow(String label, String? value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6.0), @@ -240,26 +578,28 @@ class InSituStep4Summary extends StatelessWidget { const SizedBox(width: 8), Expanded( flex: 3, - child: Text(value != null && value.isNotEmpty ? value : 'N/A', style: const TextStyle(fontSize: 16)), + child: Text(value != null && value.isNotEmpty ? value : 'N/A', + style: const TextStyle(fontSize: 16)), ), ], ), ); } - // --- 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}) { + 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(); - - // START CHANGE: Use theme-aware color for normal values - // Determine the color for the value based on theme and status. - final Color? defaultTextColor = Theme.of(context).textTheme.bodyLarge?.color; + final String displayValue = + isMissing ? 'N/A' : '${value.toStringAsFixed(5)} ${unit}'.trim(); + final Color? defaultTextColor = + Theme.of(context).textTheme.bodyLarge?.color; final Color valueColor = isOutOfBounds ? Colors.red : (isMissing ? Colors.grey : defaultTextColor ?? Colors.black); - // END CHANGE return ListTile( dense: true, @@ -275,9 +615,7 @@ class InSituStep4Summary extends StatelessWidget { ), ); } - // --- END: MODIFICATION --- - /// A reusable widget to display an attached image or a placeholder. Widget _buildImageCard(String title, File? image, {String? remark}) { final bool hasRemark = remark != null && remark.isNotEmpty; return Padding( @@ -285,12 +623,18 @@ class InSituStep4Summary extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + Text(title, + style: + const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), const SizedBox(height: 8), if (image != null) ClipRRect( borderRadius: BorderRadius.circular(8.0), - child: Image.file(image, key: UniqueKey(), height: 200, width: double.infinity, fit: BoxFit.cover), + child: Image.file(image, + key: UniqueKey(), + height: 200, + width: double.infinity, + fit: BoxFit.cover), ) else Container( @@ -300,12 +644,15 @@ class InSituStep4Summary extends StatelessWidget { color: Colors.grey[200], borderRadius: BorderRadius.circular(8.0), border: Border.all(color: Colors.grey[300]!)), - child: const Center(child: Text('No Image Attached', style: TextStyle(color: Colors.grey))), + child: const Center( + child: Text('No Image Attached', + style: TextStyle(color: Colors.grey))), ), if (hasRemark) Padding( padding: const EdgeInsets.only(top: 8.0), - child: Text('Remark: $remark', style: const TextStyle(fontStyle: FontStyle.italic)), + child: Text('Remark: $remark', + style: const TextStyle(fontStyle: FontStyle.italic)), ), ], ), diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 1635095..c581a98 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.2.01'), + subtitle: const Text('MMS Version 3.4.01'), dense: true, ), ListTile(