add in pre-sampling hub screen
This commit is contained in:
parent
8245ba0820
commit
dff653883a
@ -29,7 +29,7 @@ class _HomePageState extends State<HomePage> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: const Text("MMS Version 3.2.01"),
|
title: const Text("MMS Version 3.4.01"),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.person),
|
icon: const Icon(Icons.person),
|
||||||
|
|||||||
@ -72,13 +72,13 @@ import 'package:environment_monitoring_app/screens/river/investigative/report.da
|
|||||||
|
|
||||||
// Marine Screens
|
// Marine Screens
|
||||||
import 'package:environment_monitoring_app/screens/marine/manual/info_centre_document.dart' as marineManualInfoCentreDocument;
|
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/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/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_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_pre_departure_checklist_screen.dart' as marineManualPreDepartureChecklist;
|
||||||
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_sonde_calibration_screen.dart' as marineManualSondeCalibration;
|
||||||
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_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_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/manual/marine_image_request.dart' as marineManualImageRequest;
|
||||||
import 'package:environment_monitoring_app/screens/marine/continuous/marine_continuous_info_centre_document.dart';
|
import 'package:environment_monitoring_app/screens/marine/continuous/marine_continuous_info_centre_document.dart';
|
||||||
@ -326,14 +326,14 @@ class _RootAppState extends State<RootApp> {
|
|||||||
|
|
||||||
// Marine Manual
|
// Marine Manual
|
||||||
'/marine/manual/info': (context) => marineManualInfoCentreDocument.MarineInfoCentreDocument(),
|
'/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/in-situ': (context) => marineManualInSituSampling.MarineInSituSampling(),
|
||||||
'/marine/manual/tarball': (context) => const TarballSamplingStep1(),
|
'/marine/manual/tarball': (context) => const TarballSamplingStep1(),
|
||||||
'/marine/manual/report': (context) => const marineManualReport.MarineManualReportHomePage(),
|
'/marine/manual/report': (context) => const marineManualReport.MarineManualReportHomePage(),
|
||||||
'/marine/manual/report/npe': (context) => const marineManualNPEReport.MarineManualNPEReport(),
|
'/marine/manual/report/npe': (context) => const marineManualNPEReport.MarineManualNPEReport(),
|
||||||
'/marine/manual/report/pre-departure': (context) => const MarineManualPreDepartureChecklistScreen(),
|
'/marine/manual/report/pre-departure': (context) => const marineManualPreDepartureChecklist.MarineManualPreDepartureChecklistScreen(),
|
||||||
'/marine/manual/report/calibration': (context) => const MarineManualSondeCalibrationScreen(),
|
'/marine/manual/report/calibration': (context) => const marineManualSondeCalibration.MarineManualSondeCalibrationScreen(),
|
||||||
'/marine/manual/report/maintenance': (context) => const MarineManualEquipmentMaintenanceScreen(),
|
'/marine/manual/report/maintenance': (context) => const marineManualEquipmentMaintenance.MarineManualEquipmentMaintenanceScreen(),
|
||||||
//'/marine/manual/data-log': (context) => marineManualDataStatusLog.MarineManualDataStatusLog(), // This is handled in onGenerateRoute
|
//'/marine/manual/data-log': (context) => marineManualDataStatusLog.MarineManualDataStatusLog(), // This is handled in onGenerateRoute
|
||||||
'/marine/manual/image-request': (context) => const marineManualImageRequest.MarineImageRequestScreen(),
|
'/marine/manual/image-request': (context) => const marineManualImageRequest.MarineImageRequestScreen(),
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:convert'; // Added for jsonEncode
|
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
|
/// A data model class to hold all information for the multi-step
|
||||||
/// In-Situ Sampling form.
|
/// In-Situ Sampling form.
|
||||||
class InSituSamplingData {
|
class InSituSamplingData {
|
||||||
@ -103,6 +105,61 @@ class InSituSamplingData {
|
|||||||
this.samplingTime,
|
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<File>().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.
|
/// Creates an InSituSamplingData object from a JSON map.
|
||||||
factory InSituSamplingData.fromJson(Map<String, dynamic> json) {
|
factory InSituSamplingData.fromJson(Map<String, dynamic> json) {
|
||||||
double? doubleFromJson(dynamic value) {
|
double? doubleFromJson(dynamic value) {
|
||||||
@ -181,7 +238,7 @@ class InSituSamplingData {
|
|||||||
data.optionalImage4 = fileFromPath(json['man_optional_photo_04']);
|
data.optionalImage4 = fileFromPath(json['man_optional_photo_04']);
|
||||||
|
|
||||||
|
|
||||||
// --- START: Deserialization for NPE Fields ---
|
// --- Deserialization for NPE Fields ---
|
||||||
if (json['npe_field_observations'] is Map) {
|
if (json['npe_field_observations'] is Map) {
|
||||||
data.npeFieldObservations = Map<String, bool>.from(json['npe_field_observations']);
|
data.npeFieldObservations = Map<String, bool>.from(json['npe_field_observations']);
|
||||||
}
|
}
|
||||||
@ -193,17 +250,10 @@ class InSituSamplingData {
|
|||||||
data.npeImage2 = fileFromPath(json['npe_image_2']);
|
data.npeImage2 = fileFromPath(json['npe_image_2']);
|
||||||
data.npeImage3 = fileFromPath(json['npe_image_3']);
|
data.npeImage3 = fileFromPath(json['npe_image_3']);
|
||||||
data.npeImage4 = fileFromPath(json['npe_image_4']);
|
data.npeImage4 = fileFromPath(json['npe_image_4']);
|
||||||
// --- END: Deserialization for NPE Fields ---
|
|
||||||
|
|
||||||
return data;
|
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.
|
/// Creates a single JSON object with all submission data for offline storage.
|
||||||
Map<String, dynamic> toDbJson() {
|
Map<String, dynamic> toDbJson() {
|
||||||
return {
|
return {
|
||||||
@ -212,7 +262,6 @@ class InSituSamplingData {
|
|||||||
'secondSampler': secondSampler,
|
'secondSampler': secondSampler,
|
||||||
'sampling_date': samplingDate,
|
'sampling_date': samplingDate,
|
||||||
'sampling_time': samplingTime,
|
'sampling_time': samplingTime,
|
||||||
// ... (all other existing fields)
|
|
||||||
'sampling_type': samplingType,
|
'sampling_type': samplingType,
|
||||||
'sample_id_code': sampleIdCode,
|
'sample_id_code': sampleIdCode,
|
||||||
'selected_state_name': selectedStateName,
|
'selected_state_name': selectedStateName,
|
||||||
@ -249,18 +298,12 @@ class InSituSamplingData {
|
|||||||
'submission_status': submissionStatus,
|
'submission_status': submissionStatus,
|
||||||
'submission_message': submissionMessage,
|
'submission_message': submissionMessage,
|
||||||
'report_id': reportId,
|
'report_id': reportId,
|
||||||
|
|
||||||
// --- START: Serialization for NPE Fields ---
|
|
||||||
'npe_field_observations': npeFieldObservations,
|
'npe_field_observations': npeFieldObservations,
|
||||||
'npe_others_observation_remark': npeOthersObservationRemark,
|
'npe_others_observation_remark': npeOthersObservationRemark,
|
||||||
'npe_possible_source': npePossibleSource,
|
'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}) {
|
String generateTelegramAlertMessage({required bool isDataOnly}) {
|
||||||
final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)";
|
final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)";
|
||||||
final stationName = selectedStation?['man_station_name'] ?? 'N/A';
|
final stationName = selectedStation?['man_station_name'] ?? 'N/A';
|
||||||
|
|||||||
@ -45,12 +45,6 @@ class _MarineInSituSamplingState extends State<MarineInSituSampling> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_pageController.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();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +66,9 @@ class _MarineInSituSamplingState extends State<MarineInSituSampling> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _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<Map<String, dynamic>> _submitFormLogic() async {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||||
@ -85,17 +81,11 @@ class _MarineInSituSamplingState extends State<MarineInSituSampling> {
|
|||||||
authProvider: authProvider,
|
authProvider: authProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (mounted) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => _isLoading = false);
|
return result;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -125,7 +115,7 @@ class _MarineInSituSamplingState extends State<MarineInSituSampling> {
|
|||||||
InSituStep1SamplingInfo(data: _data, onNext: _nextPage),
|
InSituStep1SamplingInfo(data: _data, onNext: _nextPage),
|
||||||
InSituStep2SiteInfo(data: _data, onNext: _nextPage),
|
InSituStep2SiteInfo(data: _data, onNext: _nextPage),
|
||||||
InSituStep3DataCapture(data: _data, onNext: _nextPage),
|
InSituStep3DataCapture(data: _data, onNext: _nextPage),
|
||||||
InSituStep4Summary(data: _data, onSubmit: _submitForm, isLoading: _isLoading),
|
InSituStep4Summary(data: _data, onSubmit: _submitFormLogic, isLoading: _isLoading),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
108
lib/screens/marine/manual/marine_manual_pre_sampling.dart
Normal file
108
lib/screens/marine/manual/marine_manual_pre_sampling.dart
Normal file
@ -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<ReportItem> _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]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class MarinePreSampling extends StatefulWidget {
|
|
||||||
@override
|
|
||||||
State<MarinePreSampling> createState() => _MarinePreSamplingState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MarinePreSamplingState extends State<MarinePreSampling> {
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
|
||||||
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"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -86,7 +86,10 @@ class _TarballSamplingStep2State extends State<TarballSamplingStep2> {
|
|||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text("Incorrect Image Orientation"),
|
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: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text("OK"),
|
child: const Text("OK"),
|
||||||
@ -98,7 +101,11 @@ class _TarballSamplingStep2State extends State<TarballSamplingStep2> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> _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<File?> _pickAndProcessImage(ImageSource source, String imageInfo) async {
|
||||||
|
// --- END: MODIFICATION 2 ---
|
||||||
if (_isPickingImage) return null;
|
if (_isPickingImage) return null;
|
||||||
setState(() => _isPickingImage = true);
|
setState(() => _isPickingImage = true);
|
||||||
|
|
||||||
@ -117,11 +124,15 @@ class _TarballSamplingStep2State extends State<TarballSamplingStep2> {
|
|||||||
return null;
|
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();
|
_showOrientationDialog();
|
||||||
setState(() => _isPickingImage = false);
|
setState(() => _isPickingImage = false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// --- END: MODIFICATION 3 ---
|
||||||
|
|
||||||
final String watermarkTimestamp = "${widget.data.samplingDate} ${widget.data.samplingTime}";
|
final String watermarkTimestamp = "${widget.data.samplingDate} ${widget.data.samplingTime}";
|
||||||
final font = img.arial24;
|
final font = img.arial24;
|
||||||
@ -155,19 +166,18 @@ class _TarballSamplingStep2State extends State<TarballSamplingStep2> {
|
|||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
final filePath = path.join(tempDir.path, newFileName);
|
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));
|
final File processedFile = File(filePath)..writeAsBytesSync(img.encodeJpg(originalImage));
|
||||||
// --- END: MODIFICATION TO FIX RACE CONDITION ---
|
|
||||||
|
|
||||||
setState(() => _isPickingImage = false);
|
setState(() => _isPickingImage = false);
|
||||||
return processedFile;
|
return processedFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setImage(Function(File?) setImageCallback, ImageSource source, String imageInfo, {required bool isRequired}) async {
|
// --- START: MODIFICATION 4 ---
|
||||||
final file = await _pickAndProcessImage(source, imageInfo, isRequired: isRequired);
|
// 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) {
|
if (file != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
setImageCallback(file);
|
setImageCallback(file);
|
||||||
@ -200,6 +210,35 @@ class _TarballSamplingStep2State extends State<TarballSamplingStep2> {
|
|||||||
return;
|
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.optionalRemark1 = _remark1Controller.text;
|
||||||
widget.data.optionalRemark2 = _remark2Controller.text;
|
widget.data.optionalRemark2 = _remark2Controller.text;
|
||||||
widget.data.optionalRemark3 = _remark3Controller.text;
|
widget.data.optionalRemark3 = _remark3Controller.text;
|
||||||
@ -224,15 +263,13 @@ class _TarballSamplingStep2State extends State<TarballSamplingStep2> {
|
|||||||
Text("On-Site Information", style: Theme.of(context).textTheme.headlineSmall),
|
Text("On-Site Information", style: Theme.of(context).textTheme.headlineSmall),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// This dropdown now correctly consumes data from AuthProvider
|
|
||||||
Consumer<AuthProvider>(
|
Consumer<AuthProvider>(
|
||||||
builder: (context, auth, child) {
|
builder: (context, auth, child) {
|
||||||
final classifications = auth.tarballClassifications;
|
final classifications = auth.tarballClassifications;
|
||||||
// The dropdown is enabled only when the classification list is available from the local cache.
|
|
||||||
final bool isEnabled = classifications != null;
|
final bool isEnabled = classifications != null;
|
||||||
|
|
||||||
return DropdownSearch<Map<String, dynamic>>(
|
return DropdownSearch<Map<String, dynamic>>(
|
||||||
items: classifications ?? [], // Use local data from provider
|
items: classifications ?? [],
|
||||||
selectedItem: _selectedClassification,
|
selectedItem: _selectedClassification,
|
||||||
enabled: isEnabled,
|
enabled: isEnabled,
|
||||||
itemAsString: (item) => item['classification_name'] as String,
|
itemAsString: (item) => item['classification_name'] as String,
|
||||||
@ -251,7 +288,6 @@ class _TarballSamplingStep2State extends State<TarballSamplingStep2> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedClassification = value;
|
_selectedClassification = value;
|
||||||
// NECESSARY CHANGE: Save both the full object and the ID to the data model.
|
|
||||||
widget.data.selectedClassification = value;
|
widget.data.selectedClassification = value;
|
||||||
widget.data.classificationId = value?['classification_id'];
|
widget.data.classificationId = value?['classification_id'];
|
||||||
});
|
});
|
||||||
@ -321,8 +357,11 @@ class _TarballSamplingStep2State extends State<TarballSamplingStep2> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton.icon(onPressed: _isPickingImage ? null : () => _setImage(setImageCallback, ImageSource.camera, imageInfo, isRequired: isRequired), icon: const Icon(Icons.camera_alt), label: const Text("Camera")),
|
// --- START: MODIFICATION 6 ---
|
||||||
ElevatedButton.icon(onPressed: _isPickingImage ? null : () => _setImage(setImageCallback, ImageSource.gallery, imageInfo, isRequired: isRequired), icon: const Icon(Icons.photo_library), label: const Text("Gallery")),
|
// 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)
|
if (remarkController != null)
|
||||||
@ -332,7 +371,7 @@ class _TarballSamplingStep2State extends State<TarballSamplingStep2> {
|
|||||||
controller: remarkController,
|
controller: remarkController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Remarks for $title',
|
labelText: 'Remarks for $title',
|
||||||
hintText: 'Add an optional remark...',
|
hintText: 'Add a remark...', // Changed hint text to be more direct
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -6,10 +6,12 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import '../../../../auth_provider.dart';
|
import '../../../../auth_provider.dart';
|
||||||
import '../../../../models/in_situ_sampling_data.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 InSituSamplingData data;
|
||||||
final VoidCallback onSubmit;
|
final Future<Map<String, dynamic>> Function()
|
||||||
|
onSubmit; // Expects a function that returns the submission result
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
|
|
||||||
const InSituStep4Summary({
|
const InSituStep4Summary({
|
||||||
@ -19,11 +21,14 @@ class InSituStep4Summary extends StatelessWidget {
|
|||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- START: MODIFICATION FOR HIGHLIGHTING ---
|
@override
|
||||||
// Added helper logic to re-validate parameters on the summary screen.
|
State<InSituStep4Summary> createState() => _InSituStep4SummaryState();
|
||||||
|
}
|
||||||
|
|
||||||
/// Maps the app's internal parameter keys to the names used in the database.
|
class _InSituStep4SummaryState extends State<InSituStep4Summary> {
|
||||||
static const Map<String, String> _parameterKeyToLimitName = const {
|
bool _isHandlingSubmit = false;
|
||||||
|
|
||||||
|
static const Map<String, String> _parameterKeyToLimitName = {
|
||||||
'oxygenConcentration': 'Oxygen Conc',
|
'oxygenConcentration': 'Oxygen Conc',
|
||||||
'oxygenSaturation': 'Oxygen Sat',
|
'oxygenSaturation': 'Oxygen Sat',
|
||||||
'ph': 'pH',
|
'ph': 'pH',
|
||||||
@ -36,18 +41,23 @@ class InSituStep4Summary extends StatelessWidget {
|
|||||||
'batteryVoltage': 'Battery',
|
'batteryVoltage': 'Battery',
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Re-validates the final parameters against the defined limits.
|
|
||||||
Set<String> _getOutOfBoundsKeys(BuildContext context) {
|
Set<String> _getOutOfBoundsKeys(BuildContext context) {
|
||||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||||
final marineLimits = authProvider.marineParameterLimits ?? [];
|
final marineLimits = authProvider.marineParameterLimits ?? [];
|
||||||
final Set<String> invalidKeys = {};
|
final Set<String> invalidKeys = {};
|
||||||
final int? stationId = data.selectedStation?['station_id'];
|
final int? stationId = widget.data.selectedStation?['station_id'];
|
||||||
|
|
||||||
final readings = {
|
final readings = {
|
||||||
'oxygenConcentration': data.oxygenConcentration, 'oxygenSaturation': data.oxygenSaturation,
|
'oxygenConcentration': widget.data.oxygenConcentration,
|
||||||
'ph': data.ph, 'salinity': data.salinity, 'electricalConductivity': data.electricalConductivity,
|
'oxygenSaturation': widget.data.oxygenSaturation,
|
||||||
'temperature': data.temperature, 'tds': data.tds, 'turbidity': data.turbidity,
|
'ph': widget.data.ph,
|
||||||
'tss': data.tss, 'batteryVoltage': data.batteryVoltage,
|
'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) {
|
double? parseLimitValue(dynamic value) {
|
||||||
@ -63,22 +73,23 @@ class InSituStep4Summary extends StatelessWidget {
|
|||||||
final limitName = _parameterKeyToLimitName[key];
|
final limitName = _parameterKeyToLimitName[key];
|
||||||
if (limitName == null) return;
|
if (limitName == null) return;
|
||||||
|
|
||||||
// START MODIFICATION: Only check for station-specific limits
|
|
||||||
Map<String, dynamic> limitData = {};
|
Map<String, dynamic> limitData = {};
|
||||||
|
|
||||||
if (stationId != null) {
|
if (stationId != null) {
|
||||||
limitData = marineLimits.firstWhere(
|
limitData = marineLimits.firstWhere(
|
||||||
(l) => l['param_parameter_list'] == limitName && l['station_id'] == stationId,
|
(l) =>
|
||||||
|
l['param_parameter_list'] == limitName &&
|
||||||
|
l['station_id'] == stationId,
|
||||||
orElse: () => {},
|
orElse: () => {},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// END MODIFICATION
|
|
||||||
|
|
||||||
if (limitData.isNotEmpty) {
|
if (limitData.isNotEmpty) {
|
||||||
final lowerLimit = parseLimitValue(limitData['param_lower_limit']);
|
final lowerLimit = parseLimitValue(limitData['param_lower_limit']);
|
||||||
final upperLimit = parseLimitValue(limitData['param_upper_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);
|
invalidKeys.add(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,14 +97,272 @@ class InSituStep4Summary extends StatelessWidget {
|
|||||||
|
|
||||||
return invalidKeys;
|
return invalidKeys;
|
||||||
}
|
}
|
||||||
// --- END: MODIFICATION FOR HIGHLIGHTING ---
|
|
||||||
|
/// Checks captured data against NPE limits and returns detailed information for the dialog.
|
||||||
|
List<Map<String, dynamic>> _getNpeTriggeredParameters(BuildContext context) {
|
||||||
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
final npeLimits = authProvider.npeParameterLimits ?? [];
|
||||||
|
if (npeLimits.isEmpty) return [];
|
||||||
|
|
||||||
|
final List<Map<String, dynamic>> 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<bool?> _showNpeDialog(
|
||||||
|
BuildContext context, List<Map<String, dynamic>> triggeredParams) async {
|
||||||
|
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
return showDialog<bool>(
|
||||||
|
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: <Widget>[
|
||||||
|
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<void> _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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// --- START: MODIFICATION FOR HIGHLIGHTING ---
|
|
||||||
// Get the set of out-of-bounds keys before building the list.
|
|
||||||
final outOfBoundsKeys = _getOutOfBoundsKeys(context);
|
final outOfBoundsKeys = _getOutOfBoundsKeys(context);
|
||||||
// --- END: MODIFICATION FOR HIGHLIGHTING ---
|
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
@ -104,94 +373,164 @@ class InSituStep4Summary extends StatelessWidget {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
_buildSectionCard(
|
_buildSectionCard(
|
||||||
context,
|
context,
|
||||||
"Sampling & Station Details",
|
"Sampling & Station Details",
|
||||||
[
|
[
|
||||||
_buildDetailRow("1st Sampler:", data.firstSamplerName),
|
_buildDetailRow("1st Sampler:", widget.data.firstSamplerName),
|
||||||
_buildDetailRow("2nd Sampler:", data.secondSampler?['first_name']?.toString()),
|
_buildDetailRow(
|
||||||
_buildDetailRow("Sampling Date:", data.samplingDate),
|
"2nd Sampler:", widget.data.secondSampler?['first_name']?.toString()),
|
||||||
_buildDetailRow("Sampling Time:", data.samplingTime),
|
_buildDetailRow("Sampling Date:", widget.data.samplingDate),
|
||||||
_buildDetailRow("Sampling Type:", data.samplingType),
|
_buildDetailRow("Sampling Time:", widget.data.samplingTime),
|
||||||
_buildDetailRow("Sample ID Code:", data.sampleIdCode),
|
_buildDetailRow("Sampling Type:", widget.data.samplingType),
|
||||||
|
_buildDetailRow("Sample ID Code:", widget.data.sampleIdCode),
|
||||||
const Divider(height: 20),
|
const Divider(height: 20),
|
||||||
_buildDetailRow("State:", data.selectedStateName),
|
_buildDetailRow("State:", widget.data.selectedStateName),
|
||||||
_buildDetailRow("Category:", data.selectedCategoryName),
|
_buildDetailRow("Category:", widget.data.selectedCategoryName),
|
||||||
_buildDetailRow("Station Code:", data.selectedStation?['man_station_code']?.toString()),
|
_buildDetailRow("Station Code:",
|
||||||
_buildDetailRow("Station Name:", data.selectedStation?['man_station_name']?.toString()),
|
widget.data.selectedStation?['man_station_code']?.toString()),
|
||||||
_buildDetailRow("Station Location:", "${data.stationLatitude}, ${data.stationLongitude}"),
|
_buildDetailRow("Station Name:",
|
||||||
|
widget.data.selectedStation?['man_station_name']?.toString()),
|
||||||
|
_buildDetailRow("Station Location:",
|
||||||
|
"${widget.data.stationLatitude}, ${widget.data.stationLongitude}"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
_buildSectionCard(
|
_buildSectionCard(
|
||||||
context,
|
context,
|
||||||
"Location & On-Site Info",
|
"Location & On-Site Info",
|
||||||
[
|
[
|
||||||
_buildDetailRow("Current Location:", "${data.currentLatitude}, ${data.currentLongitude}"),
|
_buildDetailRow("Current Location:",
|
||||||
_buildDetailRow("Distance Difference:", data.distanceDifferenceInKm != null ? "${(data.distanceDifferenceInKm! * 1000).toStringAsFixed(0)} meters" : "N/A"),
|
"${widget.data.currentLatitude}, ${widget.data.currentLongitude}"),
|
||||||
if (data.distanceDifferenceRemarks != null && data.distanceDifferenceRemarks!.isNotEmpty)
|
_buildDetailRow(
|
||||||
_buildDetailRow("Distance Remarks:", data.distanceDifferenceRemarks),
|
"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),
|
const Divider(height: 20),
|
||||||
_buildDetailRow("Weather:", data.weather),
|
_buildDetailRow("Weather:", widget.data.weather),
|
||||||
_buildDetailRow("Tide Level:", data.tideLevel),
|
_buildDetailRow("Tide Level:", widget.data.tideLevel),
|
||||||
_buildDetailRow("Sea Condition:", data.seaCondition),
|
_buildDetailRow("Sea Condition:", widget.data.seaCondition),
|
||||||
_buildDetailRow("Event Remarks:", data.eventRemarks),
|
_buildDetailRow("Event Remarks:", widget.data.eventRemarks),
|
||||||
_buildDetailRow("Lab Remarks:", data.labRemarks),
|
_buildDetailRow("Lab Remarks:", widget.data.labRemarks),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
_buildSectionCard(
|
_buildSectionCard(
|
||||||
context,
|
context,
|
||||||
"Attached Photos",
|
"Attached Photos",
|
||||||
[
|
[
|
||||||
_buildImageCard("Left Side Land View", data.leftLandViewImage),
|
_buildImageCard("Left Side Land View", widget.data.leftLandViewImage),
|
||||||
_buildImageCard("Right Side Land View", data.rightLandViewImage),
|
_buildImageCard(
|
||||||
_buildImageCard("Filling Water into Bottle", data.waterFillingImage),
|
"Right Side Land View", widget.data.rightLandViewImage),
|
||||||
_buildImageCard("Seawater Color in Bottle", data.seawaterColorImage),
|
_buildImageCard(
|
||||||
_buildImageCard("Examine Preservative (pH paper)", data.phPaperImage),
|
"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),
|
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),
|
const SizedBox(height: 8),
|
||||||
_buildImageCard("Optional Photo 1", data.optionalImage1, remark: data.optionalRemark1),
|
_buildImageCard("Optional Photo 1", widget.data.optionalImage1,
|
||||||
_buildImageCard("Optional Photo 2", data.optionalImage2, remark: data.optionalRemark2),
|
remark: widget.data.optionalRemark1),
|
||||||
_buildImageCard("Optional Photo 3", data.optionalImage3, remark: data.optionalRemark3),
|
_buildImageCard("Optional Photo 2", widget.data.optionalImage2,
|
||||||
_buildImageCard("Optional Photo 4", data.optionalImage4, remark: data.optionalRemark4),
|
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(
|
_buildSectionCard(
|
||||||
context,
|
context,
|
||||||
"Captured Parameters",
|
"Captured Parameters",
|
||||||
[
|
[
|
||||||
_buildDetailRow("Sonde ID:", data.sondeId),
|
_buildDetailRow("Sonde ID:", widget.data.sondeId),
|
||||||
_buildDetailRow("Capture Time:", "${data.dataCaptureDate} ${data.dataCaptureTime}"),
|
_buildDetailRow("Capture Time:",
|
||||||
|
"${widget.data.dataCaptureDate} ${widget.data.dataCaptureTime}"),
|
||||||
const Divider(height: 20),
|
const Divider(height: 20),
|
||||||
// --- START: MODIFICATION FOR 5 DECIMALS & HIGHLIGHTING ---
|
_buildParameterListItem(context,
|
||||||
_buildParameterListItem(context, icon: Icons.air, label: "Oxygen Conc.", unit: "mg/L", value: data.oxygenConcentration, isOutOfBounds: outOfBoundsKeys.contains('oxygenConcentration')),
|
icon: Icons.air,
|
||||||
_buildParameterListItem(context, icon: Icons.percent, label: "Oxygen Sat.", unit: "%", value: data.oxygenSaturation, isOutOfBounds: outOfBoundsKeys.contains('oxygenSaturation')),
|
label: "Oxygen Conc.",
|
||||||
_buildParameterListItem(context, icon: Icons.science_outlined, label: "pH", unit: "", value: data.ph, isOutOfBounds: outOfBoundsKeys.contains('ph')),
|
unit: "mg/L",
|
||||||
_buildParameterListItem(context, icon: Icons.waves, label: "Salinity", unit: "ppt", value: data.salinity, isOutOfBounds: outOfBoundsKeys.contains('salinity')),
|
value: widget.data.oxygenConcentration,
|
||||||
_buildParameterListItem(context, icon: Icons.flash_on, label: "Conductivity", unit: "µS/cm", value: data.electricalConductivity, isOutOfBounds: outOfBoundsKeys.contains('electricalConductivity')),
|
isOutOfBounds:
|
||||||
_buildParameterListItem(context, icon: Icons.thermostat, label: "Temperature", unit: "°C", value: data.temperature, isOutOfBounds: outOfBoundsKeys.contains('temperature')),
|
outOfBoundsKeys.contains('oxygenConcentration')),
|
||||||
_buildParameterListItem(context, icon: Icons.grain, label: "TDS", unit: "mg/L", value: data.tds, isOutOfBounds: outOfBoundsKeys.contains('tds')),
|
_buildParameterListItem(context,
|
||||||
_buildParameterListItem(context, icon: Icons.opacity, label: "Turbidity", unit: "NTU", value: data.turbidity, isOutOfBounds: outOfBoundsKeys.contains('turbidity')),
|
icon: Icons.percent,
|
||||||
_buildParameterListItem(context, icon: Icons.filter_alt_outlined, label: "TSS", unit: "mg/L", value: data.tss, isOutOfBounds: outOfBoundsKeys.contains('tss')),
|
label: "Oxygen Sat.",
|
||||||
_buildParameterListItem(context, icon: Icons.battery_charging_full, label: "Battery", unit: "V", value: data.batteryVoltage, isOutOfBounds: outOfBoundsKeys.contains('batteryVoltage')),
|
unit: "%",
|
||||||
// --- END: MODIFICATION ---
|
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),
|
const SizedBox(height: 24),
|
||||||
isLoading
|
(widget.isLoading || _isHandlingSubmit)
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: ElevatedButton.icon(
|
: ElevatedButton.icon(
|
||||||
onPressed: onSubmit,
|
onPressed: () => _handleSubmit(context),
|
||||||
icon: const Icon(Icons.cloud_upload),
|
icon: const Icon(Icons.cloud_upload),
|
||||||
label: const Text('Confirm & Submit'),
|
label: const Text('Confirm & Submit'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
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),
|
const SizedBox(height: 16),
|
||||||
@ -199,8 +538,8 @@ class InSituStep4Summary extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A reusable widget to create a consistent section card.
|
Widget _buildSectionCard(
|
||||||
Widget _buildSectionCard(BuildContext context, String title, List<Widget> children) {
|
BuildContext context, String title, List<Widget> children) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
@ -226,7 +565,6 @@ class InSituStep4Summary extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A reusable widget for a label-value row.
|
|
||||||
Widget _buildDetailRow(String label, String? value) {
|
Widget _buildDetailRow(String label, String? value) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||||
@ -240,26 +578,28 @@ class InSituStep4Summary extends StatelessWidget {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 3,
|
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,
|
||||||
Widget _buildParameterListItem(BuildContext context, {required IconData icon, required String label, required String unit, required double? value, bool isOutOfBounds = false}) {
|
{required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required String unit,
|
||||||
|
required double? value,
|
||||||
|
bool isOutOfBounds = false}) {
|
||||||
final bool isMissing = value == null || value == -999.0;
|
final bool isMissing = value == null || value == -999.0;
|
||||||
// Format the value to 5 decimal places if it's a valid number.
|
final String displayValue =
|
||||||
final String displayValue = isMissing ? 'N/A' : '${value.toStringAsFixed(5)} ${unit}'.trim();
|
isMissing ? 'N/A' : '${value.toStringAsFixed(5)} ${unit}'.trim();
|
||||||
|
final Color? defaultTextColor =
|
||||||
// START CHANGE: Use theme-aware color for normal values
|
Theme.of(context).textTheme.bodyLarge?.color;
|
||||||
// Determine the color for the value based on theme and status.
|
|
||||||
final Color? defaultTextColor = Theme.of(context).textTheme.bodyLarge?.color;
|
|
||||||
final Color valueColor = isOutOfBounds
|
final Color valueColor = isOutOfBounds
|
||||||
? Colors.red
|
? Colors.red
|
||||||
: (isMissing ? Colors.grey : defaultTextColor ?? Colors.black);
|
: (isMissing ? Colors.grey : defaultTextColor ?? Colors.black);
|
||||||
// END CHANGE
|
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
dense: true,
|
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}) {
|
Widget _buildImageCard(String title, File? image, {String? remark}) {
|
||||||
final bool hasRemark = remark != null && remark.isNotEmpty;
|
final bool hasRemark = remark != null && remark.isNotEmpty;
|
||||||
return Padding(
|
return Padding(
|
||||||
@ -285,12 +623,18 @@ class InSituStep4Summary extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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),
|
const SizedBox(height: 8),
|
||||||
if (image != null)
|
if (image != null)
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
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
|
else
|
||||||
Container(
|
Container(
|
||||||
@ -300,12 +644,15 @@ class InSituStep4Summary extends StatelessWidget {
|
|||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
border: Border.all(color: Colors.grey[300]!)),
|
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)
|
if (hasRemark)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
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)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -748,7 +748,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.info_outline),
|
leading: const Icon(Icons.info_outline),
|
||||||
title: const Text('App Version'),
|
title: const Text('App Version'),
|
||||||
subtitle: const Text('MMS Version 3.2.01'),
|
subtitle: const Text('MMS Version 3.4.01'),
|
||||||
dense: true,
|
dense: true,
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user