updated on mms to edc data
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 32 KiB |
BIN
assets/icon_4_512x512.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
assets/icon_5_512x512.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 626 KiB After Width: | Height: | Size: 356 KiB |
|
Before Width: | Height: | Size: 998 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 27 KiB |
@ -101,7 +101,7 @@ class _HomePageState extends State<HomePage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: const Text("MMS Version 3.12.03"),
|
title: const Text("MMS Version 3.12.06"),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.person),
|
icon: const Icon(Icons.person),
|
||||||
|
|||||||
@ -32,7 +32,6 @@ class InSituSamplingData {
|
|||||||
String? weather;
|
String? weather;
|
||||||
String? tideLevel;
|
String? tideLevel;
|
||||||
String? seaCondition;
|
String? seaCondition;
|
||||||
// String? tarball; // <-- REMOVED THIS PROPERTY
|
|
||||||
String? eventRemarks;
|
String? eventRemarks;
|
||||||
String? labRemarks;
|
String? labRemarks;
|
||||||
|
|
||||||
@ -72,10 +71,6 @@ class InSituSamplingData {
|
|||||||
String? reportId;
|
String? reportId;
|
||||||
|
|
||||||
// --- START: NPE Report Compatibility Fields ---
|
// --- START: NPE Report Compatibility Fields ---
|
||||||
/// Fields to hold data that can be transferred to an NPE Report.
|
|
||||||
/// This makes the model compatible for auto-generating NPE reports in the future.
|
|
||||||
|
|
||||||
// Corresponds to the checkboxes in the NPE form
|
|
||||||
Map<String, bool> npeFieldObservations = {
|
Map<String, bool> npeFieldObservations = {
|
||||||
'Oil slick on the water surface/ Oil spill': false,
|
'Oil slick on the water surface/ Oil spill': false,
|
||||||
'Discoloration of the sea water': false,
|
'Discoloration of the sea water': false,
|
||||||
@ -88,12 +83,9 @@ class InSituSamplingData {
|
|||||||
'Foul smell': false,
|
'Foul smell': false,
|
||||||
'Others': false,
|
'Others': false,
|
||||||
};
|
};
|
||||||
// Corresponds to the "Others" text field in NPE observations
|
|
||||||
String? npeOthersObservationRemark;
|
String? npeOthersObservationRemark;
|
||||||
// Corresponds to the "Possible Source" field in the NPE form
|
|
||||||
String? npePossibleSource;
|
String? npePossibleSource;
|
||||||
|
|
||||||
// Holds the images to be attached to the NPE report
|
|
||||||
File? npeImage1;
|
File? npeImage1;
|
||||||
File? npeImage2;
|
File? npeImage2;
|
||||||
File? npeImage3;
|
File? npeImage3;
|
||||||
@ -109,48 +101,28 @@ class InSituSamplingData {
|
|||||||
/// Creates a pre-populated NPE Report object from the current In-Situ data.
|
/// Creates a pre-populated NPE Report object from the current In-Situ data.
|
||||||
MarineManualNpeReportData toNpeReportData() {
|
MarineManualNpeReportData toNpeReportData() {
|
||||||
final npeData = MarineManualNpeReportData();
|
final npeData = MarineManualNpeReportData();
|
||||||
|
|
||||||
// Transfer Reporter & Event Info
|
|
||||||
npeData.firstSamplerName = firstSamplerName;
|
npeData.firstSamplerName = firstSamplerName;
|
||||||
npeData.firstSamplerUserId = firstSamplerUserId;
|
npeData.firstSamplerUserId = firstSamplerUserId;
|
||||||
npeData.eventDate = samplingDate;
|
npeData.eventDate = samplingDate;
|
||||||
npeData.eventTime = samplingTime;
|
npeData.eventTime = samplingTime;
|
||||||
|
|
||||||
// Transfer Location Info
|
|
||||||
npeData.selectedStation = selectedStation;
|
npeData.selectedStation = selectedStation;
|
||||||
npeData.latitude = currentLatitude;
|
npeData.latitude = currentLatitude;
|
||||||
npeData.longitude = currentLongitude;
|
npeData.longitude = currentLongitude;
|
||||||
|
|
||||||
// Transfer In-Situ Measurements relevant to NPE
|
|
||||||
npeData.oxygenSaturation = oxygenSaturation;
|
npeData.oxygenSaturation = oxygenSaturation;
|
||||||
npeData.electricalConductivity = electricalConductivity;
|
npeData.electricalConductivity = electricalConductivity;
|
||||||
npeData.oxygenConcentration = oxygenConcentration;
|
npeData.oxygenConcentration = oxygenConcentration;
|
||||||
npeData.turbidity = turbidity;
|
npeData.turbidity = turbidity;
|
||||||
npeData.ph = ph;
|
npeData.ph = ph;
|
||||||
npeData.temperature = temperature;
|
npeData.temperature = temperature;
|
||||||
|
|
||||||
// Pre-populate possible source with event remarks as a starting point for the user
|
|
||||||
npeData.possibleSource = eventRemarks;
|
npeData.possibleSource = eventRemarks;
|
||||||
|
|
||||||
// Pre-populate some common observations based on data
|
if ((turbidity ?? 0) > 50) npeData.fieldObservations['Silt plume'] = true;
|
||||||
if ((turbidity ?? 0) > 50) { // Example threshold, adjust as needed
|
if ((oxygenConcentration ?? 999) < 4) npeData.fieldObservations['Foul smell'] = true;
|
||||||
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 = [
|
final availableImages = [
|
||||||
leftLandViewImage,
|
leftLandViewImage, rightLandViewImage, waterFillingImage,
|
||||||
rightLandViewImage,
|
seawaterColorImage, phPaperImage, optionalImage1,
|
||||||
waterFillingImage,
|
optionalImage2, optionalImage3, optionalImage4,
|
||||||
seawaterColorImage,
|
|
||||||
phPaperImage,
|
|
||||||
optionalImage1,
|
|
||||||
optionalImage2,
|
|
||||||
optionalImage3,
|
|
||||||
optionalImage4,
|
|
||||||
].where((img) => img != null).cast<File>().toList();
|
].where((img) => img != null).cast<File>().toList();
|
||||||
|
|
||||||
if (availableImages.isNotEmpty) npeData.image1 = availableImages[0];
|
if (availableImages.isNotEmpty) npeData.image1 = availableImages[0];
|
||||||
@ -161,34 +133,26 @@ class InSituSamplingData {
|
|||||||
return npeData;
|
return npeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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) {
|
||||||
if (value is num) return value.toDouble();
|
if (value is num) return value.toDouble();
|
||||||
if (value is String) return double.tryParse(value);
|
if (value is String) return double.tryParse(value);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int? intFromJson(dynamic value) {
|
int? intFromJson(dynamic value) {
|
||||||
if (value is int) return value;
|
if (value is int) return value;
|
||||||
if (value is String) return int.tryParse(value);
|
if (value is String) return int.tryParse(value);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
File? fileFromPath(dynamic path) => (path is String && path.isNotEmpty) ? File(path) : null;
|
||||||
File? fileFromPath(dynamic path) {
|
|
||||||
return (path is String && path.isNotEmpty) ? File(path) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final data = InSituSamplingData();
|
final data = InSituSamplingData();
|
||||||
|
|
||||||
// Standard In-Situ Fields
|
|
||||||
data.firstSamplerName = json['first_sampler_name'];
|
data.firstSamplerName = json['first_sampler_name'];
|
||||||
data.firstSamplerUserId = intFromJson(json['first_sampler_user_id']);
|
data.firstSamplerUserId = intFromJson(json['first_sampler_user_id']);
|
||||||
data.secondSampler = json['secondSampler'] ?? json['second_sampler'];
|
data.secondSampler = json['secondSampler'] ?? json['second_sampler'];
|
||||||
data.samplingDate = json['sampling_date'] ?? json['man_date'];
|
data.samplingDate = json['sampling_date'] ?? json['man_date'];
|
||||||
data.samplingTime = json['sampling_time'] ?? json['man_time'];
|
data.samplingTime = json['sampling_time'] ?? json['man_time'];
|
||||||
data.samplingType = json['sampling_type'];
|
data.samplingType = json['sampling_type'];
|
||||||
// ... (all other existing fields)
|
|
||||||
data.sampleIdCode = json['sample_id_code'];
|
data.sampleIdCode = json['sample_id_code'];
|
||||||
data.selectedStateName = json['selected_state_name'];
|
data.selectedStateName = json['selected_state_name'];
|
||||||
data.selectedCategoryName = json['selected_category_name'];
|
data.selectedCategoryName = json['selected_category_name'];
|
||||||
@ -202,7 +166,6 @@ class InSituSamplingData {
|
|||||||
data.weather = json['weather'];
|
data.weather = json['weather'];
|
||||||
data.tideLevel = json['tide_level'];
|
data.tideLevel = json['tide_level'];
|
||||||
data.seaCondition = json['sea_condition'];
|
data.seaCondition = json['sea_condition'];
|
||||||
// data.tarball = json['tarball']; // <-- REMOVED DESERIALIZATION
|
|
||||||
data.eventRemarks = json['event_remarks'];
|
data.eventRemarks = json['event_remarks'];
|
||||||
data.labRemarks = json['lab_remarks'];
|
data.labRemarks = json['lab_remarks'];
|
||||||
data.optionalRemark1 = json['man_optional_photo_01_remarks'];
|
data.optionalRemark1 = json['man_optional_photo_01_remarks'];
|
||||||
@ -226,11 +189,8 @@ class InSituSamplingData {
|
|||||||
data.submissionMessage = json['submission_message'];
|
data.submissionMessage = json['submission_message'];
|
||||||
data.reportId = json['report_id']?.toString();
|
data.reportId = json['report_id']?.toString();
|
||||||
|
|
||||||
|
|
||||||
// Image paths (handled by LocalStorageService)
|
|
||||||
data.leftLandViewImage = fileFromPath(json['man_left_side_land_view']);
|
data.leftLandViewImage = fileFromPath(json['man_left_side_land_view']);
|
||||||
data.rightLandViewImage = fileFromPath(json['man_right_side_land_view']);
|
data.rightLandViewImage = fileFromPath(json['man_right_side_land_view']);
|
||||||
// ... (all other existing images)
|
|
||||||
data.waterFillingImage = fileFromPath(json['man_filling_water_into_sample_bottle']);
|
data.waterFillingImage = fileFromPath(json['man_filling_water_into_sample_bottle']);
|
||||||
data.seawaterColorImage = fileFromPath(json['man_seawater_in_clear_glass_bottle']);
|
data.seawaterColorImage = fileFromPath(json['man_seawater_in_clear_glass_bottle']);
|
||||||
data.phPaperImage = fileFromPath(json['man_examine_preservative_ph_paper']);
|
data.phPaperImage = fileFromPath(json['man_examine_preservative_ph_paper']);
|
||||||
@ -239,15 +199,11 @@ class InSituSamplingData {
|
|||||||
data.optionalImage3 = fileFromPath(json['man_optional_photo_03']);
|
data.optionalImage3 = fileFromPath(json['man_optional_photo_03']);
|
||||||
data.optionalImage4 = fileFromPath(json['man_optional_photo_04']);
|
data.optionalImage4 = fileFromPath(json['man_optional_photo_04']);
|
||||||
|
|
||||||
|
|
||||||
// --- 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']);
|
||||||
}
|
}
|
||||||
data.npeOthersObservationRemark = json['npe_others_observation_remark'];
|
data.npeOthersObservationRemark = json['npe_others_observation_remark'];
|
||||||
data.npePossibleSource = json['npe_possible_source'];
|
data.npePossibleSource = json['npe_possible_source'];
|
||||||
|
|
||||||
// NPE image paths
|
|
||||||
data.npeImage1 = fileFromPath(json['npe_image_1']);
|
data.npeImage1 = fileFromPath(json['npe_image_1']);
|
||||||
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']);
|
||||||
@ -256,7 +212,6 @@ class InSituSamplingData {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a Map object with all submission data for local logging.
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
'first_sampler_name': firstSamplerName,
|
'first_sampler_name': firstSamplerName,
|
||||||
@ -278,7 +233,6 @@ class InSituSamplingData {
|
|||||||
'weather': weather,
|
'weather': weather,
|
||||||
'tide_level': tideLevel,
|
'tide_level': tideLevel,
|
||||||
'sea_condition': seaCondition,
|
'sea_condition': seaCondition,
|
||||||
// 'tarball': tarball, // <-- REMOVED
|
|
||||||
'event_remarks': eventRemarks,
|
'event_remarks': eventRemarks,
|
||||||
'lab_remarks': labRemarks,
|
'lab_remarks': labRemarks,
|
||||||
'man_optional_photo_01_remarks': optionalRemark1,
|
'man_optional_photo_01_remarks': optionalRemark1,
|
||||||
@ -304,143 +258,102 @@ class InSituSamplingData {
|
|||||||
'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,
|
||||||
// Image paths will be added/updated by LocalStorageService during saving/updating
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a single JSON object with all submission data, mimicking 'db.json'
|
/// Creates a single JSON object for 'db.json'. FORCING ALL VALUES TO STRING.
|
||||||
/// Creates a single JSON object with all submission data, mimicking 'db.json'
|
|
||||||
String toDbJson() {
|
String toDbJson() {
|
||||||
final data = {
|
final data = {
|
||||||
// --- Sorted exactly according to your Marine db.json sample ---
|
'battery_cap': (batteryVoltage ?? "NULL").toString(),
|
||||||
'battery_cap': batteryVoltage ?? -999.0,
|
'device_name': (sondeId ?? "").toString(),
|
||||||
'device_name': sondeId ?? "",
|
'sampling_type': (samplingType ?? "").toString(),
|
||||||
'sampling_type': samplingType ?? "",
|
'report_id': (reportId ?? "").toString(),
|
||||||
'report_id': reportId ?? "",
|
'sampler_2ndname': (secondSampler?['first_name'] ?? "").toString(),
|
||||||
'sampler_2ndname': secondSampler?['first_name'] ?? "",
|
'sample_state': (selectedStateName ?? "").toString(),
|
||||||
'sample_state': selectedStateName ?? "",
|
'station_id': (selectedStation?['man_station_code'] ?? "").toString(),
|
||||||
'station_id': selectedStation?['man_station_code'] ?? "",
|
'tech_id': (firstSamplerUserId ?? "NULL").toString(),
|
||||||
'tech_id': firstSamplerUserId ?? -999, // Default to -999 for int
|
'tech_phonenum': "",
|
||||||
'tech_phonenum': "", // Not in model, default to empty string
|
'tech_name': (firstSamplerName ?? "").toString(),
|
||||||
'tech_name': firstSamplerName ?? "",
|
'latitude': (stationLatitude ?? "").toString(),
|
||||||
'latitude': stationLatitude ?? "",
|
'longitude': (stationLongitude ?? "").toString(),
|
||||||
'longitude': stationLongitude ?? "",
|
'record_dt': (samplingDate != null && samplingTime != null) ? '$samplingDate $samplingTime' : "",
|
||||||
'record_dt': (samplingDate != null && samplingTime != null)
|
'do_mgl': (oxygenConcentration ?? "NULL").toString(),
|
||||||
? '$samplingDate $samplingTime'
|
'do_sat': (oxygenSaturation ?? "NULL").toString(),
|
||||||
: "",
|
'ph': (ph ?? "NULL").toString(),
|
||||||
|
'salinity': (salinity ?? "NULL").toString(),
|
||||||
// --- Sensor Readings ---
|
'tss': (tss ?? "NULL").toString(),
|
||||||
'do_mgl': oxygenConcentration ?? -999.0,
|
'temperature': (temperature ?? "NULL").toString(),
|
||||||
'do_sat': oxygenSaturation ?? -999.0,
|
'turbidity': (turbidity ?? "NULL").toString(),
|
||||||
'ph': ph ?? -999.0,
|
'tds': (tds ?? "NULL").toString(),
|
||||||
'salinity': salinity ?? -999.0,
|
'electric_conductivity': (electricalConductivity ?? "NULL").toString(),
|
||||||
'tss': tss ?? -999.0,
|
'sample_id': (sampleIdCode ?? "").toString(),
|
||||||
'temperature': temperature ?? -999.0,
|
'tarball': "No",
|
||||||
'turbidity': turbidity ?? -999.0,
|
'weather': (weather ?? "").toString(),
|
||||||
'tds': tds ?? -999.0,
|
'tide_lvl': (tideLevel ?? "").toString(),
|
||||||
'electric_conductivity': electricalConductivity ?? -999.0,
|
'sea_cond': (seaCondition ?? "").toString(),
|
||||||
|
'remarks_event': (eventRemarks ?? "").toString(),
|
||||||
// --- Manual/Observations ---
|
'remarks_lab': (labRemarks ?? "").toString(),
|
||||||
'sample_id': sampleIdCode ?? "",
|
|
||||||
'tarball': "No", // Field removed from model logic, default to empty
|
|
||||||
'weather': weather ?? "",
|
|
||||||
'tide_lvl': tideLevel ?? "",
|
|
||||||
'sea_cond': seaCondition ?? "",
|
|
||||||
'remarks_event': eventRemarks ?? "",
|
|
||||||
'remarks_lab': labRemarks ?? "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ❌ DO NOT UNCOMMENT. Keeps all keys even if values are null/default.
|
|
||||||
// data.removeWhere((key, value) => value == null);
|
|
||||||
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a JSON object for basic form info, mimicking 'marine_insitu_basic_form.json'.
|
/// Creates a JSON object for basic form info. FORCING ALL VALUES TO STRING.
|
||||||
String toBasicFormJson() {
|
String toBasicFormJson() {
|
||||||
final data = {
|
final data = {
|
||||||
// --- Sorted exactly according to your Marine sample ---
|
'tech_name': (firstSamplerName ?? "").toString(),
|
||||||
'tech_name': firstSamplerName ?? "",
|
'sampler_2ndname': (secondSampler?['first_name'] ?? "").toString(),
|
||||||
'sampler_2ndname': secondSampler?['first_name'] ?? "",
|
'sample_date': (samplingDate ?? "").toString(),
|
||||||
'sample_date': samplingDate ?? "",
|
'sample_time': (samplingTime ?? "").toString(),
|
||||||
'sample_time': samplingTime ?? "",
|
'sampling_type': (samplingType ?? "").toString(),
|
||||||
'sampling_type': samplingType ?? "",
|
'sample_state': (selectedStateName ?? "").toString(),
|
||||||
'sample_state': selectedStateName ?? "",
|
'sample_category': (selectedCategoryName ?? "").toString(),
|
||||||
'sample_category': selectedCategoryName ?? "", // Added to match sample
|
'station_id': (selectedStation?['man_station_code'] ?? "").toString(),
|
||||||
'station_id': selectedStation?['man_station_code'] ?? "",
|
'station_latitude': (stationLatitude ?? "").toString(),
|
||||||
'station_latitude': stationLatitude ?? "",
|
'station_longitude': (stationLongitude ?? "").toString(),
|
||||||
'station_longitude': stationLongitude ?? "",
|
'latitude': (currentLatitude ?? "").toString(),
|
||||||
'latitude': currentLatitude ?? "",
|
'longitude': (currentLongitude ?? "").toString(),
|
||||||
'longitude': currentLongitude ?? "",
|
'sample_id': (sampleIdCode ?? "").toString(),
|
||||||
'sample_id': sampleIdCode ?? "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ❌ DO NOT UNCOMMENT
|
|
||||||
// data.removeWhere((key, value) => value == null);
|
|
||||||
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a JSON object for sensor readings, mimicking 'marine_sampling_reading.json'.
|
/// Creates a JSON object for sensor readings. FORCING ALL VALUES TO STRING.
|
||||||
String toReadingJson() {
|
String toReadingJson() {
|
||||||
final data = {
|
final data = {
|
||||||
// --- Sorted exactly according to your Marine sample ---
|
'do_mgl': (oxygenConcentration ?? "NULL").toString(),
|
||||||
'do_mgl': oxygenConcentration ?? -999.0,
|
'do_sat': (oxygenSaturation ?? "NULL").toString(),
|
||||||
'do_sat': oxygenSaturation ?? -999.0,
|
'ph': (ph ?? "NULL").toString(),
|
||||||
'ph': ph ?? -999.0,
|
'salinity': (salinity ?? "NULL").toString(),
|
||||||
'salinity': salinity ?? -999.0,
|
'tds': (tds ?? "NULL").toString(),
|
||||||
'tds': tds ?? -999.0,
|
'tss': (tss ?? "NULL").toString(),
|
||||||
'tss': tss ?? -999.0,
|
'temperature': (temperature ?? "NULL").toString(),
|
||||||
'temperature': temperature ?? -999.0,
|
'turbidity': (turbidity ?? "NULL").toString(),
|
||||||
'turbidity': turbidity ?? -999.0,
|
'electric_conductivity': (electricalConductivity ?? "NULL").toString(),
|
||||||
'electric_conductivity': electricalConductivity ?? -999.0,
|
'date_sampling_reading': (dataCaptureDate ?? "").toString(),
|
||||||
'date_sampling_reading': dataCaptureDate ?? "",
|
'time_sampling_reading': (dataCaptureTime ?? "").toString(),
|
||||||
'time_sampling_reading': dataCaptureTime ?? "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ❌ DO NOT UNCOMMENT
|
|
||||||
// data.removeWhere((key, value) => value == null);
|
|
||||||
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a JSON object for manual info, mimicking 'marine_manual_info.json'.
|
/// Creates a JSON object for manual info. FORCING ALL VALUES TO STRING.
|
||||||
String toManualInfoJson() {
|
String toManualInfoJson() {
|
||||||
final data = {
|
final data = {
|
||||||
// --- Sorted exactly according to your Marine sample ---
|
'tarball': "",
|
||||||
'tarball': "", // Field removed from model logic, default to empty
|
'weather': (weather ?? "").toString(),
|
||||||
'weather': weather ?? "",
|
'tide_lvl': (tideLevel ?? "").toString(),
|
||||||
'tide_lvl': tideLevel ?? "",
|
'sea_cond': (seaCondition ?? "").toString(),
|
||||||
'sea_cond': seaCondition ?? "",
|
'remarks_event': (eventRemarks ?? "").toString(),
|
||||||
'remarks_event': eventRemarks ?? "",
|
'remarks_lab': (labRemarks ?? "").toString(),
|
||||||
'remarks_lab': labRemarks ?? "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ❌ DO NOT UNCOMMENT
|
|
||||||
// data.removeWhere((key, value) => value == null);
|
|
||||||
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> toApiFormData() {
|
Map<String, String> toApiFormData() {
|
||||||
final Map<String, String> map = {};
|
final Map<String, String> map = {};
|
||||||
|
|
||||||
void add(String key, dynamic value) {
|
void add(String key, dynamic value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
String stringValue;
|
String stringValue = (value is double) ? ((value == -999.0) ? 'NULL' : value.toStringAsFixed(5)) : value.toString();
|
||||||
if (value is double) {
|
if (stringValue.isNotEmpty) map[key] = stringValue;
|
||||||
if (value == -999.0) {
|
|
||||||
stringValue = '-999';
|
|
||||||
} else {
|
|
||||||
stringValue = value.toStringAsFixed(5);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stringValue = value.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.isNotEmpty) {
|
|
||||||
map[key] = stringValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -480,7 +393,6 @@ class InSituSamplingData {
|
|||||||
add('first_sampler_name', firstSamplerName);
|
add('first_sampler_name', firstSamplerName);
|
||||||
add('man_station_code', selectedStation?['man_station_code']);
|
add('man_station_code', selectedStation?['man_station_code']);
|
||||||
add('man_station_name', selectedStation?['man_station_name']);
|
add('man_station_name', selectedStation?['man_station_name']);
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:convert'; // Added for jsonEncode
|
import 'dart:convert'; // Added for jsonEncode
|
||||||
// REMOVED: import 'package:environment_monitoring_app/models/marine_manual_npe_report_data.dart'; // No longer needed
|
|
||||||
|
|
||||||
/// A data model class to hold all information for the multi-step
|
/// A data model class to hold all information for the multi-step
|
||||||
/// Marine Investigative Manual Sampling form.
|
/// Marine Investigative Manual Sampling form.
|
||||||
@ -80,10 +79,7 @@ class MarineInvesManualSamplingData {
|
|||||||
// --- Post-Submission Status ---
|
// --- Post-Submission Status ---
|
||||||
String? submissionStatus;
|
String? submissionStatus;
|
||||||
String? submissionMessage;
|
String? submissionMessage;
|
||||||
String? reportId; // This will be 'man_inves_id' from the DB
|
String? reportId; // This will be 'man_inves_id' from the DB OR Timestamp ID
|
||||||
|
|
||||||
// REMOVED: All NPE Report Compatibility Fields (npeFieldObservations, npeOthersObservationRemark, etc.)
|
|
||||||
|
|
||||||
|
|
||||||
MarineInvesManualSamplingData({
|
MarineInvesManualSamplingData({
|
||||||
this.samplingDate,
|
this.samplingDate,
|
||||||
@ -91,9 +87,6 @@ class MarineInvesManualSamplingData {
|
|||||||
this.stationTypeSelection = 'Existing Manual Station', // Default value
|
this.stationTypeSelection = 'Existing Manual Station', // Default value
|
||||||
});
|
});
|
||||||
|
|
||||||
// REMOVED: toNpeReportData() method
|
|
||||||
|
|
||||||
|
|
||||||
/// 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 {
|
||||||
@ -147,7 +140,6 @@ class MarineInvesManualSamplingData {
|
|||||||
'submission_status': submissionStatus,
|
'submission_status': submissionStatus,
|
||||||
'submission_message': submissionMessage,
|
'submission_message': submissionMessage,
|
||||||
'report_id': reportId,
|
'report_id': reportId,
|
||||||
// REMOVED: NPE fields from JSON
|
|
||||||
|
|
||||||
// Image paths will be added by LocalStorageService during save
|
// Image paths will be added by LocalStorageService during save
|
||||||
'inves_left_side_land_view': leftLandViewImage?.path,
|
'inves_left_side_land_view': leftLandViewImage?.path,
|
||||||
@ -177,7 +169,6 @@ class MarineInvesManualSamplingData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
File? fileFromPath(dynamic path) {
|
File? fileFromPath(dynamic path) {
|
||||||
// Ensure path is not null and not empty before creating File object
|
|
||||||
return (path is String && path.isNotEmpty) ? File(path) : null;
|
return (path is String && path.isNotEmpty) ? File(path) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,7 +177,7 @@ class MarineInvesManualSamplingData {
|
|||||||
// Step 1
|
// Step 1
|
||||||
data.firstSamplerName = json['first_sampler_name'];
|
data.firstSamplerName = json['first_sampler_name'];
|
||||||
data.firstSamplerUserId = intFromJson(json['first_sampler_user_id']);
|
data.firstSamplerUserId = intFromJson(json['first_sampler_user_id']);
|
||||||
data.secondSampler = json['secondSampler']; // Assumes it's stored correctly as JSON Map
|
data.secondSampler = json['secondSampler'];
|
||||||
data.samplingDate = json['sampling_date'];
|
data.samplingDate = json['sampling_date'];
|
||||||
data.samplingTime = json['sampling_time'];
|
data.samplingTime = json['sampling_time'];
|
||||||
data.samplingType = json['sampling_type'];
|
data.samplingType = json['sampling_type'];
|
||||||
@ -194,15 +185,15 @@ class MarineInvesManualSamplingData {
|
|||||||
data.stationTypeSelection = json['stationTypeSelection'];
|
data.stationTypeSelection = json['stationTypeSelection'];
|
||||||
data.selectedManualStateName = json['selectedManualStateName'];
|
data.selectedManualStateName = json['selectedManualStateName'];
|
||||||
data.selectedManualCategoryName = json['selectedManualCategoryName'];
|
data.selectedManualCategoryName = json['selectedManualCategoryName'];
|
||||||
data.selectedStation = json['selectedStation']; // Assumes it's stored correctly as JSON Map
|
data.selectedStation = json['selectedStation'];
|
||||||
data.selectedTarballStateName = json['selectedTarballStateName'];
|
data.selectedTarballStateName = json['selectedTarballStateName'];
|
||||||
data.selectedTarballStation = json['selectedTarballStation']; // Assumes it's stored correctly as JSON Map
|
data.selectedTarballStation = json['selectedTarballStation'];
|
||||||
data.newStationName = json['newStationName'];
|
data.newStationName = json['newStationName'];
|
||||||
data.newStationCode = json['newStationCode'];
|
data.newStationCode = json['newStationCode'];
|
||||||
data.stationLatitude = json['station_latitude']?.toString(); // Ensure conversion to String
|
data.stationLatitude = json['station_latitude']?.toString();
|
||||||
data.stationLongitude = json['station_longitude']?.toString(); // Ensure conversion to String
|
data.stationLongitude = json['station_longitude']?.toString();
|
||||||
data.currentLatitude = json['current_latitude']?.toString(); // Ensure conversion to String
|
data.currentLatitude = json['current_latitude']?.toString();
|
||||||
data.currentLongitude = json['current_longitude']?.toString(); // Ensure conversion to String
|
data.currentLongitude = json['current_longitude']?.toString();
|
||||||
data.distanceDifferenceInKm = doubleFromJson(json['distance_difference_in_km']);
|
data.distanceDifferenceInKm = doubleFromJson(json['distance_difference_in_km']);
|
||||||
data.distanceDifferenceRemarks = json['distance_difference_remarks'];
|
data.distanceDifferenceRemarks = json['distance_difference_remarks'];
|
||||||
|
|
||||||
@ -217,7 +208,7 @@ class MarineInvesManualSamplingData {
|
|||||||
data.optionalRemark3 = json['inves_optional_photo_03_remarks'];
|
data.optionalRemark3 = json['inves_optional_photo_03_remarks'];
|
||||||
data.optionalRemark4 = json['inves_optional_photo_04_remarks'];
|
data.optionalRemark4 = json['inves_optional_photo_04_remarks'];
|
||||||
|
|
||||||
// Step 2 Images (Paths stored in JSON)
|
// Step 2 Images
|
||||||
data.leftLandViewImage = fileFromPath(json['inves_left_side_land_view']);
|
data.leftLandViewImage = fileFromPath(json['inves_left_side_land_view']);
|
||||||
data.rightLandViewImage = fileFromPath(json['inves_right_side_land_view']);
|
data.rightLandViewImage = fileFromPath(json['inves_right_side_land_view']);
|
||||||
data.waterFillingImage = fileFromPath(json['inves_filling_water_into_sample_bottle']);
|
data.waterFillingImage = fileFromPath(json['inves_filling_water_into_sample_bottle']);
|
||||||
@ -246,9 +237,7 @@ class MarineInvesManualSamplingData {
|
|||||||
// Status
|
// Status
|
||||||
data.submissionStatus = json['submission_status'];
|
data.submissionStatus = json['submission_status'];
|
||||||
data.submissionMessage = json['submission_message'];
|
data.submissionMessage = json['submission_message'];
|
||||||
data.reportId = json['report_id']?.toString(); // Ensure conversion to String
|
data.reportId = json['report_id']?.toString();
|
||||||
|
|
||||||
// REMOVED: NPE fields from deserialization
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@ -262,26 +251,21 @@ class MarineInvesManualSamplingData {
|
|||||||
if (value != null) {
|
if (value != null) {
|
||||||
String stringValue;
|
String stringValue;
|
||||||
if (value is double) {
|
if (value is double) {
|
||||||
// Handle special -999.0 value
|
|
||||||
if (value == -999.0) {
|
if (value == -999.0) {
|
||||||
stringValue = '-999';
|
stringValue = '-999';
|
||||||
} else {
|
} else {
|
||||||
// Format other doubles to 5 decimal places
|
|
||||||
stringValue = value.toStringAsFixed(5);
|
stringValue = value.toStringAsFixed(5);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Convert other types directly to string
|
|
||||||
stringValue = value.toString();
|
stringValue = value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add if the resulting string is not empty
|
|
||||||
if (stringValue.isNotEmpty) {
|
if (stringValue.isNotEmpty) {
|
||||||
map[key] = stringValue;
|
map[key] = stringValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add prefix 'inves_' to all keys to match new backend endpoints
|
|
||||||
add('inves_date', samplingDate);
|
add('inves_date', samplingDate);
|
||||||
add('inves_time', samplingTime);
|
add('inves_time', samplingTime);
|
||||||
add('first_sampler_user_id', firstSamplerUserId);
|
add('first_sampler_user_id', firstSamplerUserId);
|
||||||
@ -293,23 +277,22 @@ class MarineInvesManualSamplingData {
|
|||||||
add('inves_distance_difference', distanceDifferenceInKm);
|
add('inves_distance_difference', distanceDifferenceInKm);
|
||||||
add('inves_distance_difference_remarks', distanceDifferenceRemarks);
|
add('inves_distance_difference_remarks', distanceDifferenceRemarks);
|
||||||
|
|
||||||
// --- NEW: Add station selection logic ---
|
|
||||||
add('inves_station_type', stationTypeSelection);
|
add('inves_station_type', stationTypeSelection);
|
||||||
if (stationTypeSelection == 'Existing Manual Station') {
|
if (stationTypeSelection == 'Existing Manual Station') {
|
||||||
add('station_id', selectedStation?['station_id']); // Foreign key to manual stations
|
// ✅ FIX: Ensure 'station_id' is added correctly for limit validation
|
||||||
|
add('station_id', selectedStation?['station_id'] ?? selectedStation?['man_station_id']);
|
||||||
add('inves_station_code', selectedStation?['man_station_code']);
|
add('inves_station_code', selectedStation?['man_station_code']);
|
||||||
add('inves_station_name', selectedStation?['man_station_name']);
|
add('inves_station_name', selectedStation?['man_station_name']);
|
||||||
} else if (stationTypeSelection == 'Existing Tarball Station') {
|
} else if (stationTypeSelection == 'Existing Tarball Station') {
|
||||||
add('tbl_station_id', selectedTarballStation?['station_id']); // Foreign key to tarball stations
|
add('tbl_station_id', selectedTarballStation?['station_id'] ?? selectedTarballStation?['tbl_station_id']);
|
||||||
add('inves_station_code', selectedTarballStation?['tbl_station_code']);
|
add('inves_station_code', selectedTarballStation?['tbl_station_code']);
|
||||||
add('inves_station_name', selectedTarballStation?['tbl_station_name']);
|
add('inves_station_name', selectedTarballStation?['tbl_station_name']);
|
||||||
} else if (stationTypeSelection == 'New Location') {
|
} else if (stationTypeSelection == 'New Location') {
|
||||||
add('inves_new_station_name', newStationName);
|
add('inves_new_station_name', newStationName);
|
||||||
add('inves_new_station_code', newStationCode);
|
add('inves_new_station_code', newStationCode);
|
||||||
add('inves_station_latitude', stationLatitude); // Manually entered lat
|
add('inves_station_latitude', stationLatitude);
|
||||||
add('inves_station_longitude', stationLongitude); // Manually entered lon
|
add('inves_station_longitude', stationLongitude);
|
||||||
}
|
}
|
||||||
// --- END NEW ---
|
|
||||||
|
|
||||||
add('inves_weather', weather);
|
add('inves_weather', weather);
|
||||||
add('inves_tide_level', tideLevel);
|
add('inves_tide_level', tideLevel);
|
||||||
@ -321,8 +304,8 @@ class MarineInvesManualSamplingData {
|
|||||||
add('inves_optional_photo_03_remarks', optionalRemark3);
|
add('inves_optional_photo_03_remarks', optionalRemark3);
|
||||||
add('inves_optional_photo_04_remarks', optionalRemark4);
|
add('inves_optional_photo_04_remarks', optionalRemark4);
|
||||||
add('inves_sondeID', sondeId);
|
add('inves_sondeID', sondeId);
|
||||||
add('data_capture_date', dataCaptureDate); // Note: No 'inves_' prefix assumed based on original model
|
add('data_capture_date', dataCaptureDate);
|
||||||
add('data_capture_time', dataCaptureTime); // Note: No 'inves_' prefix assumed based on original model
|
add('data_capture_time', dataCaptureTime);
|
||||||
add('inves_oxygen_conc', oxygenConcentration);
|
add('inves_oxygen_conc', oxygenConcentration);
|
||||||
add('inves_oxygen_sat', oxygenSaturation);
|
add('inves_oxygen_sat', oxygenSaturation);
|
||||||
add('inves_ph', ph);
|
add('inves_ph', ph);
|
||||||
@ -334,7 +317,7 @@ class MarineInvesManualSamplingData {
|
|||||||
add('inves_tss', tss);
|
add('inves_tss', tss);
|
||||||
add('inves_battery_volt', batteryVoltage);
|
add('inves_battery_volt', batteryVoltage);
|
||||||
|
|
||||||
add('first_sampler_name', firstSamplerName); // For logging/display purposes on backend if needed
|
add('first_sampler_name', firstSamplerName);
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
@ -342,7 +325,6 @@ class MarineInvesManualSamplingData {
|
|||||||
/// Maps image files to keys for the API submission.
|
/// Maps image files to keys for the API submission.
|
||||||
Map<String, File?> toApiImageFiles() {
|
Map<String, File?> toApiImageFiles() {
|
||||||
return {
|
return {
|
||||||
// Add prefix 'inves_' to match backend expectations
|
|
||||||
'inves_left_side_land_view': leftLandViewImage,
|
'inves_left_side_land_view': leftLandViewImage,
|
||||||
'inves_right_side_land_view': rightLandViewImage,
|
'inves_right_side_land_view': rightLandViewImage,
|
||||||
'inves_filling_water_into_sample_bottle': waterFillingImage,
|
'inves_filling_water_into_sample_bottle': waterFillingImage,
|
||||||
@ -354,8 +336,4 @@ class MarineInvesManualSamplingData {
|
|||||||
'inves_optional_photo_04': optionalImage4,
|
'inves_optional_photo_04': optionalImage4,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: REMOVED generateInvestigativeTelegramAlertMessage ---
|
|
||||||
// This logic is now handled in MarineInvestigativeSamplingService
|
|
||||||
// --- END: REMOVED ---
|
|
||||||
}
|
}
|
||||||
@ -99,7 +99,6 @@ class RiverInSituSamplingData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- START: MODIFIED FOR CONSISTENT SERIALIZATION ---
|
// --- START: MODIFIED FOR CONSISTENT SERIALIZATION ---
|
||||||
// Keys now match toMap() for reliability, with fallback to old API keys for backward compatibility.
|
|
||||||
return RiverInSituSamplingData()
|
return RiverInSituSamplingData()
|
||||||
..firstSamplerName = json['firstSamplerName'] ?? json['first_sampler_name']
|
..firstSamplerName = json['firstSamplerName'] ?? json['first_sampler_name']
|
||||||
..firstSamplerUserId = intFromJson(json['firstSamplerUserId'] ?? json['first_sampler_user_id'])
|
..firstSamplerUserId = intFromJson(json['firstSamplerUserId'] ?? json['first_sampler_user_id'])
|
||||||
@ -154,7 +153,6 @@ class RiverInSituSamplingData {
|
|||||||
..submissionStatus = json['submissionStatus']
|
..submissionStatus = json['submissionStatus']
|
||||||
..submissionMessage = json['submissionMessage']
|
..submissionMessage = json['submissionMessage']
|
||||||
..reportId = json['reportId']?.toString();
|
..reportId = json['reportId']?.toString();
|
||||||
// --- END: MODIFIED FOR CONSISTENT SERIALIZATION ---
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -165,7 +163,6 @@ class RiverInSituSamplingData {
|
|||||||
void add(String key, dynamic value) {
|
void add(String key, dynamic value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
String stringValue;
|
String stringValue;
|
||||||
// --- START FIX: Handle -999.0 correctly ---
|
|
||||||
if (value is double) {
|
if (value is double) {
|
||||||
if (value == -999.0) {
|
if (value == -999.0) {
|
||||||
stringValue = '-999';
|
stringValue = '-999';
|
||||||
@ -175,9 +172,7 @@ class RiverInSituSamplingData {
|
|||||||
} else {
|
} else {
|
||||||
stringValue = value.toString();
|
stringValue = value.toString();
|
||||||
}
|
}
|
||||||
// --- END FIX ---
|
|
||||||
|
|
||||||
// Only add non-empty values
|
|
||||||
if (stringValue.isNotEmpty) {
|
if (stringValue.isNotEmpty) {
|
||||||
map[key] = stringValue;
|
map[key] = stringValue;
|
||||||
}
|
}
|
||||||
@ -191,9 +186,7 @@ class RiverInSituSamplingData {
|
|||||||
add('r_man_time', samplingTime);
|
add('r_man_time', samplingTime);
|
||||||
add('r_man_type', samplingType);
|
add('r_man_type', samplingType);
|
||||||
add('r_man_sample_id_code', sampleIdCode);
|
add('r_man_sample_id_code', sampleIdCode);
|
||||||
// --- START FIX: Use correct key 'station_id' ---
|
|
||||||
add('station_id', selectedStation?['station_id']);
|
add('station_id', selectedStation?['station_id']);
|
||||||
// --- END FIX ---
|
|
||||||
add('r_man_current_latitude', currentLatitude);
|
add('r_man_current_latitude', currentLatitude);
|
||||||
add('r_man_current_longitude', currentLongitude);
|
add('r_man_current_longitude', currentLongitude);
|
||||||
add('r_man_distance_difference', distanceDifferenceInKm);
|
add('r_man_distance_difference', distanceDifferenceInKm);
|
||||||
@ -222,10 +215,10 @@ class RiverInSituSamplingData {
|
|||||||
add('r_man_temperature', temperature);
|
add('r_man_temperature', temperature);
|
||||||
add('r_man_tds', tds);
|
add('r_man_tds', tds);
|
||||||
add('r_man_turbidity', turbidity);
|
add('r_man_turbidity', turbidity);
|
||||||
add('r_man_ammonia', ammonia); // MODIFIED: Replaced tss with ammonia
|
add('r_man_ammonia', ammonia);
|
||||||
add('r_man_battery_volt', batteryVoltage);
|
add('r_man_battery_volt', batteryVoltage);
|
||||||
|
|
||||||
// ADDED: Flowrate fields to API form data
|
// ADDED: Flowrate fields
|
||||||
add('r_man_flowrate_method', flowrateMethod);
|
add('r_man_flowrate_method', flowrateMethod);
|
||||||
add('r_man_flowrate_sd_height', flowrateSurfaceDrifterHeight);
|
add('r_man_flowrate_sd_height', flowrateSurfaceDrifterHeight);
|
||||||
add('r_man_flowrate_sd_distance', flowrateSurfaceDrifterDistance);
|
add('r_man_flowrate_sd_distance', flowrateSurfaceDrifterDistance);
|
||||||
@ -233,13 +226,11 @@ class RiverInSituSamplingData {
|
|||||||
add('r_man_flowrate_sd_time_last', flowrateSurfaceDrifterTimeLast);
|
add('r_man_flowrate_sd_time_last', flowrateSurfaceDrifterTimeLast);
|
||||||
add('r_man_flowrate_value', flowrateValue);
|
add('r_man_flowrate_value', flowrateValue);
|
||||||
|
|
||||||
|
|
||||||
// Additional data for display or logging
|
// Additional data for display or logging
|
||||||
add('first_sampler_name', firstSamplerName);
|
add('first_sampler_name', firstSamplerName);
|
||||||
add('r_man_station_code', selectedStation?['sampling_station_code']);
|
add('r_man_station_code', selectedStation?['sampling_station_code']);
|
||||||
add('r_man_station_name', selectedStation?['sampling_river']);
|
add('r_man_station_name', selectedStation?['sampling_river']);
|
||||||
|
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,7 +248,7 @@ class RiverInSituSamplingData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ADDED: A new method to support the centralized submission logging
|
// ADDED: support for centralized submission logging
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
'firstSamplerName': firstSamplerName,
|
'firstSamplerName': firstSamplerName,
|
||||||
@ -302,7 +293,7 @@ class RiverInSituSamplingData {
|
|||||||
'temperature': temperature,
|
'temperature': temperature,
|
||||||
'tds': tds,
|
'tds': tds,
|
||||||
'turbidity': turbidity,
|
'turbidity': turbidity,
|
||||||
'ammonia': ammonia, // MODIFIED: Replaced tss with ammonia
|
'ammonia': ammonia,
|
||||||
'batteryVoltage': batteryVoltage,
|
'batteryVoltage': batteryVoltage,
|
||||||
'flowrateMethod': flowrateMethod,
|
'flowrateMethod': flowrateMethod,
|
||||||
'flowrateSurfaceDrifterHeight': flowrateSurfaceDrifterHeight,
|
'flowrateSurfaceDrifterHeight': flowrateSurfaceDrifterHeight,
|
||||||
@ -317,112 +308,87 @@ class RiverInSituSamplingData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a single JSON object with all submission data, mimicking 'db.json'
|
/// Creates a single JSON object with all submission data, mimicking 'db.json'
|
||||||
|
/// FORCING ALL VALUES TO STRING.
|
||||||
String toDbJson() {
|
String toDbJson() {
|
||||||
final data = {
|
final data = {
|
||||||
// --- Sorted exactly according to your sample ---
|
'battery_cap': (batteryVoltage ?? "NULL").toString(),
|
||||||
'battery_cap': batteryVoltage ?? -999.0,
|
'device_name': (sondeId ?? "").toString(),
|
||||||
'device_name': sondeId ?? "",
|
'sampling_type': (samplingType ?? "").toString(),
|
||||||
'sampling_type': samplingType ?? "",
|
'report_id': (reportId ?? "").toString(),
|
||||||
'report_id': reportId ?? "",
|
'sampler_2ndname': (secondSampler?['first_name'] ?? "").toString(),
|
||||||
'sampler_2ndname': secondSampler?['first_name'] ?? "",
|
'sample_state': (selectedStateName ?? "").toString(),
|
||||||
'sample_state': selectedStateName ?? "",
|
'station_id': (selectedStation?['sampling_station_code'] ?? "").toString(),
|
||||||
'station_id': selectedStation?['sampling_station_code'] ?? "",
|
'tech_id': (firstSamplerUserId ?? "NULL").toString(),
|
||||||
'tech_id': firstSamplerUserId ?? -999, // Default to -999 if no ID
|
'tech_phonenum': "",
|
||||||
'tech_phonenum': "", // Field present in sample but not in model, defaults to empty
|
'tech_name': (firstSamplerName ?? "").toString(),
|
||||||
'tech_name': firstSamplerName ?? "",
|
'latitude': (stationLatitude ?? "").toString(),
|
||||||
'latitude': stationLatitude ?? "",
|
'longitude': (stationLongitude ?? "").toString(),
|
||||||
'longitude': stationLongitude ?? "",
|
|
||||||
'record_dt': (samplingDate != null && samplingTime != null)
|
'record_dt': (samplingDate != null && samplingTime != null)
|
||||||
? '$samplingDate $samplingTime'
|
? '$samplingDate $samplingTime'
|
||||||
: "",
|
: "",
|
||||||
|
'do_mgl': (oxygenConcentration ?? "NULL").toString(),
|
||||||
// --- Sensor Readings ---
|
'do_sat': (oxygenSaturation ?? "NULL").toString(),
|
||||||
'do_mgl': oxygenConcentration ?? -999.0,
|
'ph': (ph ?? "NULL").toString(),
|
||||||
'do_sat': oxygenSaturation ?? -999.0,
|
'salinity': (salinity ?? "NULL").toString(),
|
||||||
'ph': ph ?? -999.0,
|
'temperature': (temperature ?? "NULL").toString(),
|
||||||
'salinity': salinity ?? -999.0,
|
'turbidity': (turbidity ?? "NULL").toString(),
|
||||||
'temperature': temperature ?? -999.0,
|
'tds': (tds ?? "NULL").toString(),
|
||||||
'turbidity': turbidity ?? -999.0,
|
'electric_conductivity': (electricalConductivity ?? "NULL").toString(),
|
||||||
'tds': tds ?? -999.0,
|
'flowrate': (flowrateValue ?? "NULL").toString(),
|
||||||
'electric_conductivity': electricalConductivity ?? -999.0,
|
'odour': "",
|
||||||
'flowrate': flowrateValue ?? -999.0,
|
'floatable': "",
|
||||||
|
'sample_id': (sampleIdCode ?? "").toString(),
|
||||||
// --- Manual/Observations ---
|
'weather': (weather ?? "").toString(),
|
||||||
'odour': "", // Default empty
|
'remarks_event': (eventRemarks ?? "").toString(),
|
||||||
'floatable': "", // Default empty
|
'remarks_lab': (labRemarks ?? "").toString(),
|
||||||
'sample_id': sampleIdCode ?? "",
|
|
||||||
'weather': weather ?? "",
|
|
||||||
'remarks_event': eventRemarks ?? "",
|
|
||||||
'remarks_lab': labRemarks ?? "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ❌ DO NOT UNCOMMENT. We want to keep all keys even if values are default.
|
|
||||||
// data.removeWhere((key, value) => value == null);
|
|
||||||
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a JSON object for basic form info, mimicking 'river_insitu_basic_form.json'.
|
/// Creates a JSON object for basic form info. FORCING ALL VALUES TO STRING.
|
||||||
String toBasicFormJson() {
|
String toBasicFormJson() {
|
||||||
final data = {
|
final data = {
|
||||||
// --- Sorted sequence: tech_name -> sampler_2ndname -> date/time -> type -> state -> station info -> location -> sample_id
|
'tech_name': (firstSamplerName ?? "").toString(),
|
||||||
'tech_name': firstSamplerName ?? "",
|
'sampler_2ndname': (secondSampler?['first_name'] ?? "").toString(),
|
||||||
'sampler_2ndname': secondSampler?['first_name'] ?? "",
|
'sample_date': (samplingDate ?? "").toString(),
|
||||||
'sample_date': samplingDate ?? "",
|
'sample_time': (samplingTime ?? "").toString(),
|
||||||
'sample_time': samplingTime ?? "",
|
'sampling_type': (samplingType ?? "").toString(),
|
||||||
'sampling_type': samplingType ?? "",
|
'sample_state': (selectedStateName ?? "").toString(),
|
||||||
'sample_state': selectedStateName ?? "",
|
'station_id': (selectedStation?['sampling_station_code'] ?? "").toString(),
|
||||||
'station_id': selectedStation?['sampling_station_code'] ?? "",
|
'station_latitude': (stationLatitude ?? "").toString(),
|
||||||
'station_latitude': stationLatitude ?? "",
|
'station_longitude': (stationLongitude ?? "").toString(),
|
||||||
'station_longitude': stationLongitude ?? "",
|
'latitude': (currentLatitude ?? "").toString(),
|
||||||
'latitude': currentLatitude ?? "", // Current user location lat
|
'longitude': (currentLongitude ?? "").toString(),
|
||||||
'longitude': currentLongitude ?? "", // Current user location lon
|
'sample_id': (sampleIdCode ?? "").toString(),
|
||||||
'sample_id': sampleIdCode ?? "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ❌ REMOVE or COMMENT OUT this line so no keys are deleted
|
|
||||||
// data.removeWhere((key, value) => value == null);
|
|
||||||
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a JSON object for sensor readings, mimicking 'river_sampling_reading.json'.
|
/// Creates a JSON object for sensor readings. FORCING ALL VALUES TO STRING.
|
||||||
/// Creates a JSON object for sensor readings, mimicking 'river_sampling_reading.json'.
|
|
||||||
String toReadingJson() {
|
String toReadingJson() {
|
||||||
final data = {
|
final data = {
|
||||||
// --- Sorted exactly according to your sample ---
|
'do_mgl': (oxygenConcentration ?? "NULL").toString(),
|
||||||
'do_mgl': oxygenConcentration ?? -999.0,
|
'do_sat': (oxygenSaturation ?? "NULL").toString(),
|
||||||
'do_sat': oxygenSaturation ?? -999.0,
|
'ph': (ph ?? "NULL").toString(),
|
||||||
'ph': ph ?? -999.0,
|
'salinity': (salinity ?? "NULL").toString(),
|
||||||
'salinity': salinity ?? -999.0,
|
'temperature': (temperature ?? "NULL").toString(),
|
||||||
'temperature': temperature ?? -999.0,
|
'turbidity': (turbidity ?? "NULL").toString(),
|
||||||
'turbidity': turbidity ?? -999.0,
|
'tds': (tds ?? "NULL").toString(),
|
||||||
'tds': tds ?? -999.0,
|
'electric_conductivity': (electricalConductivity ?? "NULL").toString(),
|
||||||
'electric_conductivity': electricalConductivity ?? -999.0,
|
'flowrate': (flowrateValue ?? "NULL").toString(),
|
||||||
'flowrate': flowrateValue ?? -999.0,
|
'date_sampling_reading': (dataCaptureDate ?? "").toString(),
|
||||||
|
'time_sampling_reading': (dataCaptureTime ?? "").toString(),
|
||||||
// --- Date and Time ---
|
|
||||||
'date_sampling_reading': dataCaptureDate ?? "",
|
|
||||||
'time_sampling_reading': dataCaptureTime ?? "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ❌ REMOVE or COMMENT OUT this line to ensure no keys are skipped/removed
|
|
||||||
// data.removeWhere((key, value) => value == null);
|
|
||||||
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a JSON object for manual info, mimicking 'river_manual_info.json'.
|
/// Creates a JSON object for manual info. FORCING ALL VALUES TO STRING.
|
||||||
String toManualInfoJson() {
|
String toManualInfoJson() {
|
||||||
final data = {
|
final data = {
|
||||||
// --- START FIX: Map model properties to correct manual info keys ---
|
'weather': (weather ?? "").toString(),
|
||||||
'weather': weather,
|
'remarks_event': (eventRemarks ?? "").toString(),
|
||||||
'remarks_event': eventRemarks,
|
'remarks_lab': (labRemarks ?? "").toString(),
|
||||||
'remarks_lab': labRemarks,
|
|
||||||
// --- END FIX ---
|
|
||||||
};
|
};
|
||||||
// Remove null values before encoding
|
|
||||||
data.removeWhere((key, value) => value == null);
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -422,101 +422,98 @@ class RiverInvesManualSamplingData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a single JSON object for FTP 'db.json', mimicking River In-Situ structure.
|
/// Creates a single JSON object for FTP 'db.json', forcing every value to String.
|
||||||
String toDbJson() {
|
String toDbJson() {
|
||||||
final data = {
|
final data = {
|
||||||
'battery_cap': batteryVoltage == -999.0 ? null : batteryVoltage,
|
'battery_cap': (batteryVoltage ?? "").toString(),
|
||||||
'device_name': sondeId,
|
'device_name': (sondeId ?? "").toString(),
|
||||||
'sampling_type': samplingType, // 'Investigative'
|
'sampling_type': (samplingType ?? "Investigative").toString(),
|
||||||
'report_id': reportId,
|
'report_id': (reportId ?? "").toString(),
|
||||||
'sampler_2ndname': secondSampler?['first_name'],
|
'sampler_2ndname': (secondSampler?['first_name'] ?? "").toString(),
|
||||||
'sample_state': getDeterminedStateName(), // Use determined state
|
'sample_state': (getDeterminedStateName() ?? "").toString(),
|
||||||
'station_id': getDeterminedStationCode(), // Use determined code
|
'station_id': (getDeterminedStationCode() ?? "").toString(),
|
||||||
'tech_id': firstSamplerUserId,
|
'tech_id': (firstSamplerUserId ?? "NULL").toString(),
|
||||||
'tech_name': firstSamplerName,
|
'tech_phonenum': "NULL",
|
||||||
'latitude': stationLatitude, // Use captured/selected station lat
|
'tech_name': (firstSamplerName ?? "").toString(),
|
||||||
'longitude': stationLongitude, // Use captured/selected station lon
|
'latitude': (stationLatitude ?? "").toString(),
|
||||||
|
'longitude': (stationLongitude ?? "").toString(),
|
||||||
'record_dt': '$samplingDate $samplingTime',
|
'record_dt': '$samplingDate $samplingTime',
|
||||||
'do_mgl': oxygenConcentration == -999.0 ? null : oxygenConcentration,
|
'do_mgl': (oxygenConcentration ?? -999.0).toString(),
|
||||||
'do_sat': oxygenSaturation == -999.0 ? null : oxygenSaturation,
|
'do_sat': (oxygenSaturation ?? -999.0).toString(),
|
||||||
'ph': ph == -999.0 ? null : ph,
|
'ph': (ph ?? -999.0).toString(),
|
||||||
'salinity': salinity == -999.0 ? null : salinity,
|
'salinity': (salinity ?? -999.0).toString(),
|
||||||
'temperature': temperature == -999.0 ? null : temperature,
|
'temperature': (temperature ?? -999.0).toString(),
|
||||||
'turbidity': turbidity == -999.0 ? null : turbidity,
|
'turbidity': (turbidity ?? -999.0).toString(),
|
||||||
'tds': tds == -999.0 ? null : tds,
|
'tds': (tds ?? -999.0).toString(),
|
||||||
'electric_conductivity': electricalConductivity == -999.0 ? null : electricalConductivity,
|
'electric_conductivity': (electricalConductivity ?? -999.0).toString(),
|
||||||
'ammonia': ammonia == -999.0 ? null : ammonia,
|
'tss': (ammonia ?? 0.0).toString(), // Mapped ammonia to 'tss' key for FTP consistency
|
||||||
'flowrate': flowrateValue ?? -999.0,
|
'flowrate': (flowrateValue ?? -999.0).toString(),
|
||||||
'odour': '', // Not collected
|
'odour': '',
|
||||||
'floatable': '', // Not collected
|
'floatable': '',
|
||||||
'sample_id': sampleIdCode,
|
'sample_id': (sampleIdCode ?? "").toString(),
|
||||||
'weather': weather,
|
'weather': (weather ?? "").toString(),
|
||||||
'remarks_event': eventRemarks,
|
'remarks_event': (eventRemarks ?? "").toString(),
|
||||||
'remarks_lab': labRemarks,
|
'remarks_lab': (labRemarks ?? "").toString(),
|
||||||
// --- Add Investigative Specific fields if needed by FTP structure ---
|
'station_type': (stationTypeSelection ?? "").toString(),
|
||||||
'station_type': stationTypeSelection, // e.g., 'New Location'
|
'new_basin': (newBasinName ?? "").toString(),
|
||||||
'new_basin': stationTypeSelection == 'New Location' ? newBasinName : null,
|
'new_river': (newRiverName ?? "").toString(),
|
||||||
'new_river': stationTypeSelection == 'New Location' ? newRiverName : null,
|
'new_station_name': (newStationName ?? "").toString(),
|
||||||
'new_station_name': stationTypeSelection == 'New Location' ? newStationName : null, // Include newStationName
|
'tarball': "No",
|
||||||
|
'tide_lvl': "",
|
||||||
|
'sea_cond': "",
|
||||||
};
|
};
|
||||||
data.removeWhere((key, value) => value == null);
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates JSON for FTP 'river_inves_basic_form.json' (mimicking In-Situ).
|
/// Creates JSON for FTP 'river_inves_basic_form.json', forcing every value to String.
|
||||||
String toBasicFormJson() {
|
String toBasicFormJson() {
|
||||||
final data = {
|
final data = {
|
||||||
'tech_name': firstSamplerName,
|
'tech_name': (firstSamplerName ?? "").toString(),
|
||||||
'sampler_2ndname': secondSampler?['first_name'],
|
'sampler_2ndname': (secondSampler?['first_name'] ?? "").toString(),
|
||||||
'sample_date': samplingDate,
|
'sample_date': (samplingDate ?? "").toString(),
|
||||||
'sample_time': samplingTime,
|
'sample_time': (samplingTime ?? "").toString(),
|
||||||
'sampling_type': samplingType, // 'Investigative'
|
'sampling_type': (samplingType ?? "Investigative").toString(),
|
||||||
'sample_state': getDeterminedStateName(),
|
'sample_state': (getDeterminedStateName() ?? "").toString(),
|
||||||
'station_id': getDeterminedStationCode(),
|
'station_id': (getDeterminedStationCode() ?? "").toString(),
|
||||||
'station_latitude': stationLatitude,
|
'station_latitude': (stationLatitude ?? "").toString(),
|
||||||
'station_longitude': stationLongitude,
|
'station_longitude': (stationLongitude ?? "").toString(),
|
||||||
'latitude': currentLatitude, // Current location lat
|
'latitude': (currentLatitude ?? "").toString(),
|
||||||
'longitude': currentLongitude, // Current location lon
|
'longitude': (currentLongitude ?? "").toString(),
|
||||||
'sample_id': sampleIdCode,
|
'sample_id': (sampleIdCode ?? "").toString(),
|
||||||
// --- Add Investigative Specific fields if needed ---
|
'station_type': (stationTypeSelection ?? "").toString(),
|
||||||
'station_type': stationTypeSelection,
|
'new_basin': (newBasinName ?? "").toString(),
|
||||||
'new_basin': stationTypeSelection == 'New Location' ? newBasinName : null,
|
'new_river': (newRiverName ?? "").toString(),
|
||||||
'new_river': stationTypeSelection == 'New Location' ? newRiverName : null,
|
'new_station_name': (newStationName ?? "").toString(),
|
||||||
'new_station_name': stationTypeSelection == 'New Location' ? newStationName : null, // Include newStationName
|
|
||||||
};
|
};
|
||||||
//data.removeWhere((key, value) => value == null);
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates JSON for FTP 'river_inves_reading.json' (mimicking In-Situ).
|
/// Creates JSON for FTP 'river_inves_reading.json', forcing every value to String.
|
||||||
String toReadingJson() {
|
String toReadingJson() {
|
||||||
final data = {
|
final data = {
|
||||||
'do_mgl': oxygenConcentration == -999.0 ? null : oxygenConcentration,
|
'do_mgl': (oxygenConcentration ?? -999.0).toString(),
|
||||||
'do_sat': oxygenSaturation == -999.0 ? null : oxygenSaturation,
|
'do_sat': (oxygenSaturation ?? -999.0).toString(),
|
||||||
'ph': ph == -999.0 ? null : ph,
|
'ph': (ph ?? -999.0).toString(),
|
||||||
'salinity': salinity == -999.0 ? null : salinity,
|
'salinity': (salinity ?? -999.0).toString(),
|
||||||
'temperature': temperature == -999.0 ? null : temperature,
|
'temperature': (temperature ?? -999.0).toString(),
|
||||||
'turbidity': turbidity == -999.0 ? null : turbidity,
|
'turbidity': (turbidity ?? -999.0).toString(),
|
||||||
'tds': tds == -999.0 ? null : tds,
|
'tds': (tds ?? -999.0).toString(),
|
||||||
'electric_conductivity': electricalConductivity == -999.0 ? null : electricalConductivity,
|
'electric_conductivity': (electricalConductivity ?? -999.0).toString(),
|
||||||
'ammonia': ammonia == -999.0 ? null : ammonia,
|
'tss': (ammonia ?? -999.0).toString(),
|
||||||
'flowrate': flowrateValue,
|
'flowrate': (flowrateValue ?? -999.0).toString(),
|
||||||
'date_sampling_reading': dataCaptureDate,
|
'date_sampling_reading': (dataCaptureDate ?? "").toString(),
|
||||||
'time_sampling_reading': dataCaptureTime,
|
'time_sampling_reading': (dataCaptureTime ?? "").toString(),
|
||||||
};
|
};
|
||||||
data.removeWhere((key, value) => value == null);
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates JSON for FTP 'river_inves_manual_info.json' (mimicking In-Situ).
|
/// Creates JSON for FTP 'river_inves_manual_info.json', forcing every value to String.
|
||||||
String toManualInfoJson() {
|
String toManualInfoJson() {
|
||||||
final data = {
|
final data = {
|
||||||
'weather': weather,
|
'weather': (weather ?? "").toString(),
|
||||||
'remarks_event': eventRemarks,
|
'remarks_event': (eventRemarks ?? "").toString(),
|
||||||
'remarks_lab': labRemarks,
|
'remarks_lab': (labRemarks ?? "").toString(),
|
||||||
};
|
};
|
||||||
data.removeWhere((key, value) => value == null);
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// lib/models/river_manual_triennial_sampling_data.dart
|
// lib/models/river_manual_triennial_sampling_data.dart
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:convert'; // Added for jsonEncode
|
import 'dart:convert';
|
||||||
|
|
||||||
/// Data model for the River Manual Triennial Sampling form.
|
/// Data model for the River Manual Triennial Sampling form.
|
||||||
class RiverManualTriennialSamplingData {
|
class RiverManualTriennialSamplingData {
|
||||||
@ -160,7 +160,7 @@ class RiverManualTriennialSamplingData {
|
|||||||
String stringValue;
|
String stringValue;
|
||||||
if (value is double) {
|
if (value is double) {
|
||||||
if (value == -999.0) {
|
if (value == -999.0) {
|
||||||
stringValue = '-999';
|
stringValue = 'NULL';
|
||||||
} else {
|
} else {
|
||||||
stringValue = value.toStringAsFixed(5);
|
stringValue = value.toStringAsFixed(5);
|
||||||
}
|
}
|
||||||
@ -299,106 +299,96 @@ class RiverManualTriennialSamplingData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a single JSON object with all submission data, mimicking 'db.json'
|
/// Creates a single JSON object with all submission data.
|
||||||
|
/// Every value is explicitly converted to a String for the API.
|
||||||
String toDbJson() {
|
String toDbJson() {
|
||||||
final data = {
|
final data = {
|
||||||
'battery_cap': batteryVoltage == -999.0 ? null : batteryVoltage,
|
'battery_cap': (batteryVoltage ?? "NULL").toString(),
|
||||||
'device_name': sondeId,
|
'device_name': (sondeId ?? "").toString(),
|
||||||
'sampling_type': samplingType,
|
'sampling_type': (samplingType ?? "").toString(),
|
||||||
'report_id': reportId,
|
'report_id': (reportId ?? "").toString(),
|
||||||
'sampler_2ndname': secondSampler?['first_name'],
|
'sampler_2ndname': (secondSampler?['first_name'] ?? "").toString(),
|
||||||
'sample_state': selectedStateName,
|
'sample_state': (selectedStateName ?? "").toString(),
|
||||||
'station_id': selectedStation?['sampling_station_code'],
|
'station_id': (selectedStation?['sampling_station_code'] ?? "").toString(),
|
||||||
'tech_id': firstSamplerUserId,
|
'tech_id': (firstSamplerUserId ?? "NULL").toString(),
|
||||||
'tech_name': firstSamplerName,
|
'tech_phonenum': "NULL",
|
||||||
'latitude': stationLatitude,
|
'tech_name': (firstSamplerName ?? "").toString(),
|
||||||
'longitude': stationLongitude,
|
'latitude': (stationLatitude ?? "").toString(),
|
||||||
|
'longitude': (stationLongitude ?? "").toString(),
|
||||||
'record_dt': '$samplingDate $samplingTime',
|
'record_dt': '$samplingDate $samplingTime',
|
||||||
'do_mgl': oxygenConcentration == -999.0 ? null : oxygenConcentration,
|
'do_mgl': (oxygenConcentration ?? "NULL").toString(),
|
||||||
'do_sat': oxygenSaturation == -999.0 ? null : oxygenSaturation,
|
'do_sat': (oxygenSaturation ?? "NULL").toString(),
|
||||||
'ph': ph == -999.0 ? null : ph,
|
'ph': (ph ?? "NULL").toString(),
|
||||||
'salinity': salinity == -999.0 ? null : salinity,
|
'salinity': (salinity ?? "NULL").toString(),
|
||||||
'temperature': temperature == -999.0 ? null : temperature,
|
'temperature': (temperature ?? "NULL").toString(),
|
||||||
'turbidity': turbidity == -999.0 ? null : turbidity,
|
'turbidity': (turbidity ?? "NULL").toString(),
|
||||||
'tds': tds == -999.0 ? null : tds,
|
'tds': (tds ?? "NULL").toString(),
|
||||||
'electric_conductivity': electricalConductivity == -999.0 ? null : electricalConductivity,
|
'electric_conductivity': (electricalConductivity ?? "NULL").toString(),
|
||||||
//'ammonia': ammonia == -999.0 ? null : ammonia,
|
'tss': (ammonia ?? "NULL").toString(),
|
||||||
'flowrate': flowrateValue,
|
'flowrate': (flowrateValue ?? "NULL").toString(),
|
||||||
'odour': '', // Not collected
|
'odour': '',
|
||||||
'floatable': '', // Not collected
|
'floatable': '',
|
||||||
'sample_id': sampleIdCode,
|
'sample_id': (sampleIdCode ?? "").toString(),
|
||||||
'weather': weather,
|
'weather': (weather ?? "").toString(),
|
||||||
'remarks_event': eventRemarks,
|
'remarks_event': (eventRemarks ?? "").toString(),
|
||||||
'remarks_lab': labRemarks,
|
'remarks_lab': (labRemarks ?? "").toString(),
|
||||||
|
'tarball': "No",
|
||||||
|
'tide_lvl': "",
|
||||||
|
'sea_cond': "",
|
||||||
};
|
};
|
||||||
//data.removeWhere((key, value) => value == null);
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a JSON object for basic form info, mimicking 'river_insitu_basic_form.json'.
|
/// Creates a JSON object for basic form info.
|
||||||
|
/// Every value is explicitly converted to a String.
|
||||||
String toBasicFormJson() {
|
String toBasicFormJson() {
|
||||||
final data = {
|
final data = {
|
||||||
// Keys sorted exactly according to the provided sample
|
'tech_name': (firstSamplerName ?? "").toString(),
|
||||||
'tech_name': firstSamplerName ?? "",
|
'sampler_2ndname': (secondSampler?['first_name'] ?? "").toString(),
|
||||||
'sampler_2ndname': secondSampler?['first_name'] ?? "",
|
'sample_date': (samplingDate ?? "").toString(),
|
||||||
'sample_date': samplingDate ?? "",
|
'sample_time': (samplingTime ?? "").toString(),
|
||||||
'sample_time': samplingTime ?? "",
|
'sampling_type': (samplingType ?? "").toString(),
|
||||||
'sampling_type': samplingType ?? "",
|
'sample_state': (selectedStateName ?? "").toString(),
|
||||||
'sample_state': selectedStateName ?? "",
|
'station_id': (selectedStation?['sampling_station_code'] ?? "").toString(),
|
||||||
'station_id': selectedStation?['sampling_station_code'] ?? "",
|
'station_latitude': (stationLatitude ?? "").toString(),
|
||||||
'station_latitude': stationLatitude ?? "",
|
'station_longitude': (stationLongitude ?? "").toString(),
|
||||||
'station_longitude': stationLongitude ?? "",
|
'latitude': (currentLatitude ?? "").toString(),
|
||||||
'latitude': currentLatitude ?? "", // Current user location lat
|
'longitude': (currentLongitude ?? "").toString(),
|
||||||
'longitude': currentLongitude ?? "", // Current user location lon
|
'sample_id': (sampleIdCode ?? "").toString(),
|
||||||
'sample_id': sampleIdCode ?? "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ❌ REMOVE or COMMENT OUT this line so no keys are deleted
|
|
||||||
// data.removeWhere((key, value) => value == null);
|
|
||||||
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a JSON object for sensor readings, mimicking 'river_sampling_reading.json'.
|
/// Creates a JSON object for sensor readings.
|
||||||
|
/// Every value is explicitly converted to a String.
|
||||||
String toReadingJson() {
|
String toReadingJson() {
|
||||||
final data = {
|
final data = {
|
||||||
// Use ?? operator to default to -999.0 if null
|
'do_mgl': (oxygenConcentration ?? "NULL").toString(),
|
||||||
'do_mgl': oxygenConcentration ?? -999.0,
|
'do_sat': (oxygenSaturation ?? "NULL").toString(),
|
||||||
'do_sat': oxygenSaturation ?? -999.0,
|
'ph': (ph ?? "NULL").toString(),
|
||||||
'ph': ph ?? -999.0,
|
'salinity': (salinity ?? "NULL").toString(),
|
||||||
'salinity': salinity ?? -999.0,
|
'temperature': (temperature ?? "NULL").toString(),
|
||||||
'temperature': temperature ?? -999.0,
|
'turbidity': (turbidity ?? "NULL").toString(),
|
||||||
'turbidity': turbidity ?? -999.0,
|
'tds': (tds ?? "NULL").toString(),
|
||||||
'tds': tds ?? -999.0,
|
'electric_conductivity': (electricalConductivity ?? "NULL").toString(),
|
||||||
'electric_conductivity': electricalConductivity ?? -999.0,
|
'tss': (ammonia ?? "NULL").toString(),
|
||||||
|
'flowrate': (flowrateValue ?? "NULL").toString(),
|
||||||
// Keep 'ammonia' commented out if it's not used in Triennial,
|
'date_sampling_reading': (dataCaptureDate ?? "").toString(),
|
||||||
// otherwise uncomment and use: 'ammonia': ammonia ?? -999.0,
|
'time_sampling_reading': (dataCaptureTime ?? "").toString(),
|
||||||
|
|
||||||
// Flowrate defaults to -999.0 (as requested in previous turn)
|
|
||||||
'flowrate': flowrateValue ?? -999.0,
|
|
||||||
|
|
||||||
// Date and Time default to empty string "" if null
|
|
||||||
'date_sampling_reading': dataCaptureDate ?? "",
|
|
||||||
'time_sampling_reading': dataCaptureTime ?? "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ❌ REMOVE or COMMENT OUT this line so keys are NEVER deleted
|
|
||||||
// data.removeWhere((key, value) => value == null);
|
|
||||||
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a JSON object for manual info, mimicking 'river_manual_info.json'.
|
/// Creates a JSON object for manual info.
|
||||||
|
/// Every value is explicitly converted to a String.
|
||||||
String toManualInfoJson() {
|
String toManualInfoJson() {
|
||||||
final data = {
|
final data = {
|
||||||
// --- START FIX: Map model properties to correct manual info keys ---
|
'weather': (weather ?? "").toString(),
|
||||||
'weather': weather,
|
'remarks_event': (eventRemarks ?? "").toString(),
|
||||||
'remarks_event': eventRemarks,
|
'remarks_lab': (labRemarks ?? "").toString(),
|
||||||
'remarks_lab': labRemarks,
|
|
||||||
// --- END FIX ---
|
|
||||||
};
|
};
|
||||||
data.removeWhere((key, value) => value == null);
|
|
||||||
return jsonEncode(data);
|
return jsonEncode(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:connectivity_plus/connectivity_plus.dart'; // Keep for potential future use, though not strictly necessary for the new logic
|
import 'package:connectivity_plus/connectivity_plus.dart'; // Keep for potential future use, though not strictly necessary for the new logic
|
||||||
|
|
||||||
import 'package:environment_monitoring_app/services/api_service.dart';
|
import 'package:environment_monitoring_app/services/api_service.dart';
|
||||||
|
import 'package:environment_monitoring_app/services/user_preferences_service.dart';
|
||||||
import 'package:environment_monitoring_app/auth_provider.dart';
|
import 'package:environment_monitoring_app/auth_provider.dart';
|
||||||
import 'package:environment_monitoring_app/home_page.dart';
|
import 'package:environment_monitoring_app/home_page.dart';
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
final TextEditingController _passwordController = TextEditingController();
|
final TextEditingController _passwordController = TextEditingController();
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String _errorMessage = '';
|
String _errorMessage = '';
|
||||||
|
String _loadingMessage = ''; // To show dynamic status updates
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -39,6 +41,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_errorMessage = '';
|
_errorMessage = '';
|
||||||
|
_loadingMessage = 'Authenticating...';
|
||||||
});
|
});
|
||||||
|
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
@ -58,10 +61,42 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
// --- Online Success ---
|
// --- Online Success ---
|
||||||
final String token = result['data']['token'];
|
final String token = result['data']['token'];
|
||||||
final Map<String, dynamic> profile = result['data']['profile'];
|
final Map<String, dynamic> profile = result['data']['profile'];
|
||||||
|
|
||||||
await auth.login(token, profile, password);
|
await auth.login(token, profile, password);
|
||||||
|
|
||||||
|
// --- FIRST TIME VS SUBSEQUENT LOGIN LOGIC ---
|
||||||
if (auth.isFirstLogin) {
|
if (auth.isFirstLogin) {
|
||||||
await auth.setIsFirstLogin(false);
|
// >> FIRST TIME: BLOCKING WAIT REQUIRED <<
|
||||||
|
// We must ensure configs are present before the user reaches the dashboard
|
||||||
|
// so that the default routing (API/FTP) is set up correctly.
|
||||||
|
setState(() {
|
||||||
|
_loadingMessage = 'Setting up environment...\nFetching server configurations...';
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugPrint("First time login: Fetching initial configurations (Blocking)...");
|
||||||
|
|
||||||
|
// 1. Fetch raw config data from server (API & FTP tables)
|
||||||
|
await apiService.fetchInitialConfigurations();
|
||||||
|
|
||||||
|
// 2. Apply default logic to "tick" the active servers
|
||||||
|
// We await this so the DB is ready before navigation.
|
||||||
|
await UserPreferencesService().applyAndSaveDefaultPreferencesIfNeeded();
|
||||||
|
|
||||||
|
// 3. Mark first login as done
|
||||||
|
await auth.setIsFirstLogin(false);
|
||||||
|
|
||||||
|
debugPrint("First time setup complete.");
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Warning: Initial setup encountered an error: $e");
|
||||||
|
// Proceed anyway; user can sync manually later if needed.
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// >> SUBSEQUENT LOGIN: NO BLOCKING <<
|
||||||
|
// User wants fast access. We skip the 'await' for configs.
|
||||||
|
// The app will rely on local data immediately.
|
||||||
|
// The HomePage will handle background syncing later.
|
||||||
|
debugPrint("Subsequent login: Skipping blocking config fetch. Using local data.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@ -72,6 +107,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
// --- Online Failure (API Error) ---
|
// --- Online Failure (API Error) ---
|
||||||
setState(() {
|
setState(() {
|
||||||
_errorMessage = result['message'] ?? 'Invalid email or password.';
|
_errorMessage = result['message'] ?? 'Invalid email or password.';
|
||||||
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
_showSnackBar(_errorMessage, isError: true);
|
_showSnackBar(_errorMessage, isError: true);
|
||||||
}
|
}
|
||||||
@ -86,21 +122,22 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
_showSnackBar("Connection failed. Trying offline login...", isError: true);
|
_showSnackBar("Connection failed. Trying offline login...", isError: true);
|
||||||
// FIX: Removed the unreliable connectivity check. Treat all exceptions here as a reason to try offline.
|
// FIX: Removed the unreliable connectivity check. Treat all exceptions here as a reason to try offline.
|
||||||
await _attemptOfflineLogin(auth, email, password);
|
await _attemptOfflineLogin(auth, email, password);
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// --- END: MODIFIED Internet-First Strategy ---
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function to perform offline validation and update UI.
|
/// Helper function to perform offline validation and update UI.
|
||||||
Future<void> _attemptOfflineLogin(AuthProvider auth, String email, String password) async {
|
Future<void> _attemptOfflineLogin(AuthProvider auth, String email, String password) async {
|
||||||
|
setState(() {
|
||||||
|
_loadingMessage = 'Verifying offline credentials...';
|
||||||
|
});
|
||||||
|
|
||||||
final bool offlineSuccess = await auth.loginOffline(email, password);
|
final bool offlineSuccess = await auth.loginOffline(email, password);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
|
||||||
if (offlineSuccess) {
|
if (offlineSuccess) {
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(builder: (context) => const HomePage()),
|
MaterialPageRoute(builder: (context) => const HomePage()),
|
||||||
@ -141,7 +178,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
"PSTW MMS V4",
|
"PSTW MMS",
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@ -166,7 +203,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
image: const DecorationImage(
|
image: const DecorationImage(
|
||||||
image: AssetImage('assets/icon_3_512x512.png'),
|
image: AssetImage('assets/icon_4_512x512.png'),
|
||||||
fit: BoxFit.cover, // Ensures the image fills the circle
|
fit: BoxFit.cover, // Ensures the image fills the circle
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -189,7 +226,17 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_isLoading
|
_isLoading
|
||||||
? const CircularProgressIndicator()
|
? Column(
|
||||||
|
children: [
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
_loadingMessage,
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
: ElevatedButton(
|
: ElevatedButton(
|
||||||
onPressed: _login,
|
onPressed: _login,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|||||||
@ -37,15 +37,15 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
late final TextEditingController _currentLatController;
|
late final TextEditingController _currentLatController;
|
||||||
late final TextEditingController _currentLonController;
|
late final TextEditingController _currentLonController;
|
||||||
|
|
||||||
// --- NEW: Controllers for 'New Location' ---
|
// Controllers for 'New Location'
|
||||||
late final TextEditingController _newStationNameController;
|
late final TextEditingController _newStationNameController;
|
||||||
late final TextEditingController _newStationCodeController;
|
late final TextEditingController _newStationCodeController;
|
||||||
|
|
||||||
// --- NEW: State for Station Selection ---
|
// State for Station Selection
|
||||||
String _stationType = 'Existing Manual Station';
|
String _stationType = 'Existing Manual Station';
|
||||||
final List<String> _stationTypeOptions = ['Existing Manual Station', 'Existing Tarball Station', 'New Location'];
|
final List<String> _stationTypeOptions = ['Existing Manual Station', 'Existing Tarball Station', 'New Location'];
|
||||||
|
|
||||||
// --- Lists for Dropdowns ---
|
// Lists for Dropdowns
|
||||||
List<String> _manualStatesList = [];
|
List<String> _manualStatesList = [];
|
||||||
List<String> _categoriesForManualState = [];
|
List<String> _categoriesForManualState = [];
|
||||||
List<Map<String, dynamic>> _stationsForManualCategory = [];
|
List<Map<String, dynamic>> _stationsForManualCategory = [];
|
||||||
@ -107,7 +107,7 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
|
|
||||||
_stationType = widget.data.stationTypeSelection ?? 'Existing Manual Station';
|
_stationType = widget.data.stationTypeSelection ?? 'Existing Manual Station';
|
||||||
|
|
||||||
// --- Load Manual Station Data ---
|
// Load Manual Station Data
|
||||||
final allManualStations = auth.manualStations ?? [];
|
final allManualStations = auth.manualStations ?? [];
|
||||||
if (allManualStations.isNotEmpty) {
|
if (allManualStations.isNotEmpty) {
|
||||||
_manualStatesList = allManualStations.map((s) => s['state_name'] as String?).whereType<String>().toSet().toList()..sort();
|
_manualStatesList = allManualStations.map((s) => s['state_name'] as String?).whereType<String>().toSet().toList()..sort();
|
||||||
@ -125,7 +125,7 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Load Tarball Station Data ---
|
// Load Tarball Station Data
|
||||||
final allTarballStations = auth.tarballStations ?? [];
|
final allTarballStations = auth.tarballStations ?? [];
|
||||||
if (allTarballStations.isNotEmpty) {
|
if (allTarballStations.isNotEmpty) {
|
||||||
_tarballStatesList = allTarballStations.map((s) => s['state_name'] as String?).whereType<String>().toSet().toList()..sort();
|
_tarballStatesList = allTarballStations.map((s) => s['state_name'] as String?).whereType<String>().toSet().toList()..sort();
|
||||||
@ -137,14 +137,12 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// --- NEW: Handle Station Type Change ---
|
|
||||||
void _handleStationTypeChange(String? value) {
|
void _handleStationTypeChange(String? value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_stationType = value;
|
_stationType = value;
|
||||||
widget.data.stationTypeSelection = value;
|
widget.data.stationTypeSelection = value;
|
||||||
|
|
||||||
// Clear all station-related data to avoid conflicts
|
|
||||||
widget.data.selectedManualStateName = null;
|
widget.data.selectedManualStateName = null;
|
||||||
widget.data.selectedManualCategoryName = null;
|
widget.data.selectedManualCategoryName = null;
|
||||||
widget.data.selectedStation = null;
|
widget.data.selectedStation = null;
|
||||||
@ -171,23 +169,23 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
final service = Provider.of<MarineInvestigativeSamplingService>(context, listen: false);
|
final service = Provider.of<MarineInvestigativeSamplingService>(context, listen: false);
|
||||||
try {
|
try {
|
||||||
final position = await service.getCurrentLocation();
|
final position = await service.getCurrentLocation();
|
||||||
if (mounted) {
|
// FIX: Check if mounted after async location call to prevent framework crash
|
||||||
setState(() {
|
if (!mounted) return;
|
||||||
widget.data.currentLatitude = position.latitude.toString();
|
|
||||||
widget.data.currentLongitude = position.longitude.toString();
|
|
||||||
_currentLatController.text = widget.data.currentLatitude!;
|
|
||||||
_currentLonController.text = widget.data.currentLongitude!;
|
|
||||||
|
|
||||||
// If 'New Location' is selected, also populate the station lat/lon
|
setState(() {
|
||||||
if (_stationType == 'New Location') {
|
widget.data.currentLatitude = position.latitude.toString();
|
||||||
widget.data.stationLatitude = widget.data.currentLatitude;
|
widget.data.currentLongitude = position.longitude.toString();
|
||||||
widget.data.stationLongitude = widget.data.currentLongitude;
|
_currentLatController.text = widget.data.currentLatitude!;
|
||||||
_stationLatController.text = widget.data.stationLatitude!;
|
_currentLonController.text = widget.data.currentLongitude!;
|
||||||
_stationLonController.text = widget.data.stationLongitude!;
|
|
||||||
}
|
if (_stationType == 'New Location') {
|
||||||
_calculateDistance();
|
widget.data.stationLatitude = widget.data.currentLatitude;
|
||||||
});
|
widget.data.stationLongitude = widget.data.currentLongitude;
|
||||||
}
|
_stationLatController.text = widget.data.stationLatitude!;
|
||||||
|
_stationLonController.text = widget.data.stationLongitude!;
|
||||||
|
}
|
||||||
|
_calculateDistance();
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if(mounted) {
|
if(mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to get location: $e')));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to get location: $e')));
|
||||||
@ -219,12 +217,12 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.data.distanceDifferenceInKm = null; // Clear distance if coords invalid
|
widget.data.distanceDifferenceInKm = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.data.distanceDifferenceInKm = null; // Clear distance if coords missing
|
widget.data.distanceDifferenceInKm = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,6 +232,7 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => const SimpleBarcodeScannerPage()),
|
MaterialPageRoute(builder: (context) => const SimpleBarcodeScannerPage()),
|
||||||
);
|
);
|
||||||
|
// FIX: Ensure widget is still mounted before updating state after returning from navigation
|
||||||
if (result is String && result != '-1' && mounted) {
|
if (result is String && result != '-1' && mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.data.sampleIdCode = result;
|
widget.data.sampleIdCode = result;
|
||||||
@ -242,10 +241,10 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Re-used from original for manual stations ---
|
|
||||||
Future<void> _findAndShowNearbyStations() async {
|
Future<void> _findAndShowNearbyStations() async {
|
||||||
if (widget.data.currentLatitude == null || widget.data.currentLatitude!.isEmpty) {
|
if (widget.data.currentLatitude == null || widget.data.currentLatitude!.isEmpty) {
|
||||||
await _getCurrentLocation();
|
await _getCurrentLocation();
|
||||||
|
// FIX: Standard async mount check
|
||||||
if (!mounted || widget.data.currentLatitude == null || widget.data.currentLatitude!.isEmpty) {
|
if (!mounted || widget.data.currentLatitude == null || widget.data.currentLatitude!.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -256,26 +255,24 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
|
|
||||||
final currentLat = double.parse(widget.data.currentLatitude!);
|
final currentLat = double.parse(widget.data.currentLatitude!);
|
||||||
final currentLon = double.parse(widget.data.currentLongitude!);
|
final currentLon = double.parse(widget.data.currentLongitude!);
|
||||||
final allStations = auth.manualStations ?? []; // Only search manual stations
|
final allStations = auth.manualStations ?? [];
|
||||||
final List<Map<String, dynamic>> nearbyStations = [];
|
final List<Map<String, dynamic>> nearbyStations = [];
|
||||||
|
|
||||||
for (var station in allStations) {
|
for (var station in allStations) {
|
||||||
final stationLat = station['man_latitude'];
|
final stationLat = station['man_latitude'];
|
||||||
final stationLon = station['man_longitude'];
|
final stationLon = station['man_longitude'];
|
||||||
|
|
||||||
// Ensure coordinates are numbers before calculating distance
|
|
||||||
if (stationLat is num && stationLon is num) {
|
if (stationLat is num && stationLon is num) {
|
||||||
final distance = service.calculateDistance(currentLat, currentLon, stationLat.toDouble(), stationLon.toDouble());
|
final distance = service.calculateDistance(currentLat, currentLon, stationLat.toDouble(), stationLon.toDouble());
|
||||||
if (distance <= 5.0) { // 5km radius
|
if (distance <= 5.0) {
|
||||||
nearbyStations.add({'station': station, 'distance': distance});
|
nearbyStations.add({'station': station, 'distance': distance});
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
debugPrint("Skipping station ${station['man_station_code']} due to invalid coordinates: Lat=$stationLat, Lon=$stationLon");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nearbyStations.sort((a, b) => a['distance'].compareTo(b['distance']));
|
nearbyStations.sort((a, b) => a['distance'].compareTo(b['distance']));
|
||||||
|
|
||||||
|
// FIX: Verify mount before showing dialog
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
final selectedStation = await showDialog<Map<String, dynamic>>(
|
final selectedStation = await showDialog<Map<String, dynamic>>(
|
||||||
@ -283,19 +280,17 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
builder: (context) => _NearbyStationsDialog(nearbyStations: nearbyStations),
|
builder: (context) => _NearbyStationsDialog(nearbyStations: nearbyStations),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedStation != null) {
|
// FIX: Verify mount after dialog closes
|
||||||
|
if (selectedStation != null && mounted) {
|
||||||
_updateFormWithSelectedStation(selectedStation);
|
_updateFormWithSelectedStation(selectedStation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Re-used from original for manual stations ---
|
|
||||||
void _updateFormWithSelectedStation(Map<String, dynamic> station) {
|
void _updateFormWithSelectedStation(Map<String, dynamic> station) {
|
||||||
final allStations = Provider.of<AuthProvider>(context, listen: false).manualStations ?? [];
|
final allStations = Provider.of<AuthProvider>(context, listen: false).manualStations ?? [];
|
||||||
setState(() {
|
setState(() {
|
||||||
// Update State
|
|
||||||
widget.data.selectedManualStateName = station['state_name'];
|
widget.data.selectedManualStateName = station['state_name'];
|
||||||
|
|
||||||
// Update Category List based on new State
|
|
||||||
final categories = allStations
|
final categories = allStations
|
||||||
.where((s) => s['state_name'] == widget.data.selectedManualStateName)
|
.where((s) => s['state_name'] == widget.data.selectedManualStateName)
|
||||||
.map((s) => s['category_name'] as String?)
|
.map((s) => s['category_name'] as String?)
|
||||||
@ -305,10 +300,8 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
categories.sort();
|
categories.sort();
|
||||||
_categoriesForManualState = categories;
|
_categoriesForManualState = categories;
|
||||||
|
|
||||||
// Update Category
|
|
||||||
widget.data.selectedManualCategoryName = station['category_name'];
|
widget.data.selectedManualCategoryName = station['category_name'];
|
||||||
|
|
||||||
// Update Station List based on new State and Category
|
|
||||||
_stationsForManualCategory = allStations
|
_stationsForManualCategory = allStations
|
||||||
.where((s) =>
|
.where((s) =>
|
||||||
s['state_name'] == widget.data.selectedManualStateName &&
|
s['state_name'] == widget.data.selectedManualStateName &&
|
||||||
@ -316,14 +309,12 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
.toList()
|
.toList()
|
||||||
..sort((a, b) => (a['man_station_code'] ?? '').compareTo(b['man_station_code'] ?? ''));
|
..sort((a, b) => (a['man_station_code'] ?? '').compareTo(b['man_station_code'] ?? ''));
|
||||||
|
|
||||||
// Update Selected Station and its coordinates
|
|
||||||
widget.data.selectedStation = station;
|
widget.data.selectedStation = station;
|
||||||
widget.data.stationLatitude = station['man_latitude']?.toString();
|
widget.data.stationLatitude = station['man_latitude']?.toString();
|
||||||
widget.data.stationLongitude = station['man_longitude']?.toString();
|
widget.data.stationLongitude = station['man_longitude']?.toString();
|
||||||
_stationLatController.text = widget.data.stationLatitude ?? '';
|
_stationLatController.text = widget.data.stationLatitude ?? '';
|
||||||
_stationLonController.text = widget.data.stationLongitude ?? '';
|
_stationLonController.text = widget.data.stationLongitude ?? '';
|
||||||
|
|
||||||
// Recalculate distance
|
|
||||||
_calculateDistance();
|
_calculateDistance();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -332,16 +323,12 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
_formKey.currentState!.save();
|
_formKey.currentState!.save();
|
||||||
|
|
||||||
// The distance check applies to all 3 types.
|
|
||||||
// For "New Location", it compares manually-entered Lat/Lon vs. Current Lat/Lon.
|
|
||||||
final distanceInMeters = (widget.data.distanceDifferenceInKm ?? 0) * 1000;
|
final distanceInMeters = (widget.data.distanceDifferenceInKm ?? 0) * 1000;
|
||||||
|
|
||||||
// Only show remark dialog if distance is > 50m AND station lat/lon are actually set
|
|
||||||
// (prevents dialog for 'New Location' before coords are entered/fetched)
|
|
||||||
if (distanceInMeters > 50 && widget.data.stationLatitude != null && widget.data.stationLongitude != null) {
|
if (distanceInMeters > 50 && widget.data.stationLatitude != null && widget.data.stationLongitude != null) {
|
||||||
_showDistanceRemarkDialog();
|
_showDistanceRemarkDialog();
|
||||||
} else {
|
} else {
|
||||||
widget.data.distanceDifferenceRemarks = null; // Clear remarks if within limit
|
widget.data.distanceDifferenceRemarks = null;
|
||||||
widget.onNext();
|
widget.onNext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -364,7 +351,7 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text('Your current location is more than 50m away from the selected/entered station location.'),
|
const Text('Your current location is more than 50m away from the station.'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: remarkController,
|
controller: remarkController,
|
||||||
@ -388,19 +375,18 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Cancel'),
|
child: const Text('Cancel'),
|
||||||
onPressed: () {
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
child: const Text('Confirm'),
|
child: const Text('Confirm'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (dialogFormKey.currentState!.validate()) {
|
// FIX: Guard setState and navigation within dialog actions
|
||||||
|
if (dialogFormKey.currentState!.validate() && mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.data.distanceDifferenceRemarks = remarkController.text;
|
widget.data.distanceDifferenceRemarks = remarkController.text;
|
||||||
});
|
});
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
widget.onNext(); // Proceed after confirming remark
|
widget.onNext();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -452,7 +438,6 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// --- NEW: Station Type Selection ---
|
|
||||||
Text("Station Selection", style: Theme.of(context).textTheme.titleLarge),
|
Text("Station Selection", style: Theme.of(context).textTheme.titleLarge),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
@ -460,11 +445,10 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
items: _stationTypeOptions.map((type) => DropdownMenuItem(value: type, child: Text(type))).toList(),
|
items: _stationTypeOptions.map((type) => DropdownMenuItem(value: type, child: Text(type))).toList(),
|
||||||
onChanged: _handleStationTypeChange,
|
onChanged: _handleStationTypeChange,
|
||||||
decoration: const InputDecoration(labelText: 'Station Source *'),
|
decoration: const InputDecoration(labelText: 'Station Source *'),
|
||||||
validator: (value) => value == null ? 'Please select a station source' : null, // Added validator
|
validator: (value) => value == null ? 'Please select a station source' : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// --- NEW: Conditional Station Widgets ---
|
|
||||||
if (_stationType == 'Existing Manual Station')
|
if (_stationType == 'Existing Manual Station')
|
||||||
_buildManualStationSelectors(auth.manualStations ?? []),
|
_buildManualStationSelectors(auth.manualStations ?? []),
|
||||||
|
|
||||||
@ -474,7 +458,6 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
if (_stationType == 'New Location')
|
if (_stationType == 'New Location')
|
||||||
_buildNewLocationFields(),
|
_buildNewLocationFields(),
|
||||||
|
|
||||||
// --- Location Verification (Common to all) ---
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text("Location Verification", style: Theme.of(context).textTheme.titleLarge),
|
Text("Location Verification", style: Theme.of(context).textTheme.titleLarge),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@ -514,13 +497,10 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
onPressed: _isLoadingLocation ? null : _getCurrentLocation,
|
onPressed: _isLoadingLocation ? null : _getCurrentLocation,
|
||||||
icon: _isLoadingLocation ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.location_searching),
|
icon: _isLoadingLocation ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.location_searching),
|
||||||
label: Text(_stationType == 'New Location' ? "Get & Use Current Location" : "Get Current Location"),
|
label: Text(_stationType == 'New Location' ? "Get & Use Current Location" : "Get Current Location"),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12), // Consistent padding
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// --- Sample ID (Common to all) ---
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _barcodeController,
|
controller: _barcodeController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@ -532,7 +512,7 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
),
|
),
|
||||||
validator: (val) => val == null || val.isEmpty ? "Sample ID is required" : null,
|
validator: (val) => val == null || val.isEmpty ? "Sample ID is required" : null,
|
||||||
onSaved: (val) => widget.data.sampleIdCode = val,
|
onSaved: (val) => widget.data.sampleIdCode = val,
|
||||||
onChanged: (val) => widget.data.sampleIdCode = val, // Update data model on change
|
onChanged: (val) => widget.data.sampleIdCode = val,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
@ -540,13 +520,12 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
|
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
|
||||||
child: const Text('Next'),
|
child: const Text('Next'),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16), // Add padding at the bottom
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// --- Widget builder for Manual Station selection ---
|
|
||||||
Widget _buildManualStationSelectors(List<Map<String, dynamic>> allStations) {
|
Widget _buildManualStationSelectors(List<Map<String, dynamic>> allStations) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@ -563,22 +542,16 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
_stationLatController.clear();
|
_stationLatController.clear();
|
||||||
_stationLonController.clear();
|
_stationLonController.clear();
|
||||||
widget.data.distanceDifferenceInKm = null;
|
widget.data.distanceDifferenceInKm = null;
|
||||||
|
|
||||||
// --- CORRECTED LOGIC ---
|
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
_categoriesForManualState = allStations
|
_categoriesForManualState = allStations
|
||||||
.where((s) => s['state_name'] == state)
|
.where((s) => s['state_name'] == state)
|
||||||
.map((s) => s['category_name'] as String?)
|
.map((s) => s['category_name'] as String?)
|
||||||
.whereType<String>()
|
.whereType<String>()
|
||||||
.toSet()
|
.toSet().toList()..sort();
|
||||||
.toList();
|
|
||||||
_categoriesForManualState.sort(); // Sort after creating the list
|
|
||||||
} else {
|
} else {
|
||||||
_categoriesForManualState = <String>[];
|
_categoriesForManualState = <String>[];
|
||||||
}
|
}
|
||||||
// --- END CORRECTION ---
|
_stationsForManualCategory = [];
|
||||||
|
|
||||||
_stationsForManualCategory = []; // Clear stations list
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
validator: (val) => val == null ? "State is required" : null,
|
validator: (val) => val == null ? "State is required" : null,
|
||||||
@ -597,17 +570,13 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
_stationLatController.clear();
|
_stationLatController.clear();
|
||||||
_stationLonController.clear();
|
_stationLonController.clear();
|
||||||
widget.data.distanceDifferenceInKm = null;
|
widget.data.distanceDifferenceInKm = null;
|
||||||
|
|
||||||
// --- CORRECTED LOGIC (Similar structure) ---
|
|
||||||
if (category != null) {
|
if (category != null) {
|
||||||
_stationsForManualCategory = allStations
|
_stationsForManualCategory = allStations
|
||||||
.where((s) => s['state_name'] == widget.data.selectedManualStateName && s['category_name'] == category)
|
.where((s) => s['state_name'] == widget.data.selectedManualStateName && s['category_name'] == category)
|
||||||
.toList();
|
.toList()..sort((a, b) => (a['man_station_code'] ?? '').compareTo(b['man_station_code'] ?? ''));
|
||||||
_stationsForManualCategory.sort((a, b) => (a['man_station_code'] ?? '').compareTo(b['man_station_code'] ?? '')); // Sort after creating
|
|
||||||
} else {
|
} else {
|
||||||
_stationsForManualCategory = [];
|
_stationsForManualCategory = [];
|
||||||
}
|
}
|
||||||
// --- END CORRECTION ---
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
validator: (val) => widget.data.selectedManualStateName != null && val == null ? "Category is required" : null,
|
validator: (val) => widget.data.selectedManualStateName != null && val == null ? "Category is required" : null,
|
||||||
@ -626,7 +595,7 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
widget.data.stationLongitude = station?['man_longitude']?.toString();
|
widget.data.stationLongitude = station?['man_longitude']?.toString();
|
||||||
_stationLatController.text = widget.data.stationLatitude ?? '';
|
_stationLatController.text = widget.data.stationLatitude ?? '';
|
||||||
_stationLonController.text = widget.data.stationLongitude ?? '';
|
_stationLonController.text = widget.data.stationLongitude ?? '';
|
||||||
_calculateDistance(); // Recalculate distance when station changes
|
_calculateDistance();
|
||||||
}),
|
}),
|
||||||
validator: (val) => widget.data.selectedManualCategoryName != null && val == null ? "Station is required" : null,
|
validator: (val) => widget.data.selectedManualCategoryName != null && val == null ? "Station is required" : null,
|
||||||
),
|
),
|
||||||
@ -645,7 +614,6 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// --- Widget builder for Tarball Station selection ---
|
|
||||||
Widget _buildTarballStationSelectors(List<Map<String, dynamic>> allStations) {
|
Widget _buildTarballStationSelectors(List<Map<String, dynamic>> allStations) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@ -661,17 +629,13 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
_stationLatController.clear();
|
_stationLatController.clear();
|
||||||
_stationLonController.clear();
|
_stationLonController.clear();
|
||||||
widget.data.distanceDifferenceInKm = null;
|
widget.data.distanceDifferenceInKm = null;
|
||||||
|
|
||||||
// --- CORRECTED LOGIC ---
|
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
_stationsForTarballState = allStations
|
_stationsForTarballState = allStations
|
||||||
.where((s) => s['state_name'] == state)
|
.where((s) => s['state_name'] == state)
|
||||||
.toList();
|
.toList()..sort((a, b) => (a['tbl_station_code'] ?? '').compareTo(b['tbl_station_code'] ?? ''));
|
||||||
_stationsForTarballState.sort((a, b) => (a['tbl_station_code'] ?? '').compareTo(b['tbl_station_code'] ?? '')); // Sort after creating
|
|
||||||
} else {
|
} else {
|
||||||
_stationsForTarballState = <Map<String, dynamic>>[];
|
_stationsForTarballState = <Map<String, dynamic>>[];
|
||||||
}
|
}
|
||||||
// --- END CORRECTION ---
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
validator: (val) => val == null ? "State is required" : null,
|
validator: (val) => val == null ? "State is required" : null,
|
||||||
@ -690,7 +654,7 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
widget.data.stationLongitude = station?['tbl_longitude']?.toString();
|
widget.data.stationLongitude = station?['tbl_longitude']?.toString();
|
||||||
_stationLatController.text = widget.data.stationLatitude ?? '';
|
_stationLatController.text = widget.data.stationLatitude ?? '';
|
||||||
_stationLonController.text = widget.data.stationLongitude ?? '';
|
_stationLonController.text = widget.data.stationLongitude ?? '';
|
||||||
_calculateDistance(); // Recalculate distance when station changes
|
_calculateDistance();
|
||||||
}),
|
}),
|
||||||
validator: (val) => widget.data.selectedTarballStateName != null && val == null ? "Station is required" : null,
|
validator: (val) => widget.data.selectedTarballStateName != null && val == null ? "Station is required" : null,
|
||||||
),
|
),
|
||||||
@ -702,7 +666,6 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// --- Widget builder for New Location fields ---
|
|
||||||
Widget _buildNewLocationFields() {
|
Widget _buildNewLocationFields() {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@ -711,7 +674,7 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
decoration: const InputDecoration(labelText: 'New Station Name *'),
|
decoration: const InputDecoration(labelText: 'New Station Name *'),
|
||||||
validator: (val) => val == null || val.isEmpty ? "Station Name is required" : null,
|
validator: (val) => val == null || val.isEmpty ? "Station Name is required" : null,
|
||||||
onSaved: (val) => widget.data.newStationName = val,
|
onSaved: (val) => widget.data.newStationName = val,
|
||||||
onChanged: (val) => widget.data.newStationName = val, // Update data model on change
|
onChanged: (val) => widget.data.newStationName = val,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@ -719,7 +682,7 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
decoration: const InputDecoration(labelText: 'New Station Code *', hintText: "e.g., INV-001"),
|
decoration: const InputDecoration(labelText: 'New Station Code *', hintText: "e.g., INV-001"),
|
||||||
validator: (val) => val == null || val.isEmpty ? "Station Code is required" : null,
|
validator: (val) => val == null || val.isEmpty ? "Station Code is required" : null,
|
||||||
onSaved: (val) => widget.data.newStationCode = val,
|
onSaved: (val) => widget.data.newStationCode = val,
|
||||||
onChanged: (val) => widget.data.newStationCode = val, // Update data model on change
|
onChanged: (val) => widget.data.newStationCode = val,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@ -734,8 +697,8 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
},
|
},
|
||||||
onSaved: (val) => widget.data.stationLatitude = val,
|
onSaved: (val) => widget.data.stationLatitude = val,
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
widget.data.stationLatitude = val; // Update data model on change
|
widget.data.stationLatitude = val;
|
||||||
_calculateDistance(); // Recalculate distance when manually changed
|
_calculateDistance();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@ -751,8 +714,8 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
},
|
},
|
||||||
onSaved: (val) => widget.data.stationLongitude = val,
|
onSaved: (val) => widget.data.stationLongitude = val,
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
widget.data.stationLongitude = val; // Update data model on change
|
widget.data.stationLongitude = val;
|
||||||
_calculateDistance(); // Recalculate distance when manually changed
|
_calculateDistance();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -760,7 +723,6 @@ class _MarineInvesManualStep1SamplingInfoState extends State<MarineInvesManualSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Re-used Dialog Widget for Nearby Stations ---
|
|
||||||
class _NearbyStationsDialog extends StatelessWidget {
|
class _NearbyStationsDialog extends StatelessWidget {
|
||||||
final List<Map<String, dynamic>> nearbyStations;
|
final List<Map<String, dynamic>> nearbyStations;
|
||||||
|
|
||||||
@ -773,7 +735,7 @@ class _NearbyStationsDialog extends StatelessWidget {
|
|||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: double.maxFinite,
|
width: double.maxFinite,
|
||||||
child: nearbyStations.isEmpty
|
child: nearbyStations.isEmpty
|
||||||
? const Center(child: Text('No stations found within 5km of your current location.')) // More informative text
|
? const Center(child: Text('No stations found within 5km of your current location.'))
|
||||||
: ListView.builder(
|
: ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: nearbyStations.length,
|
itemCount: nearbyStations.length,
|
||||||
@ -783,13 +745,13 @@ class _NearbyStationsDialog extends StatelessWidget {
|
|||||||
final distanceInMeters = (item['distance'] as double) * 1000;
|
final distanceInMeters = (item['distance'] as double) * 1000;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4.0), // Add vertical margin
|
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text("${station['man_station_code'] ?? 'N/A'}"),
|
title: Text("${station['man_station_code'] ?? 'N/A'}"),
|
||||||
subtitle: Text("${station['man_station_name'] ?? 'N/A'}"),
|
subtitle: Text("${station['man_station_name'] ?? 'N/A'}"),
|
||||||
trailing: Text("${distanceInMeters.toStringAsFixed(0)} m"),
|
trailing: Text("${distanceInMeters.toStringAsFixed(0)} m"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop(station); // Return selected station
|
Navigator.of(context).pop(station);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -798,7 +760,7 @@ class _NearbyStationsDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(), // Return null on cancel
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
child: const Text('Cancel'),
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart
|
// lib/screens/marine/investigative/manual_sampling/marine_inves_manual_step_2_site_info.dart
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data'; // <-- ADDED: Required for Uint8List
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -27,7 +27,7 @@ class MarineInvesManualStep2SiteInfo extends StatefulWidget {
|
|||||||
|
|
||||||
class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2SiteInfo> {
|
class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2SiteInfo> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
bool _isPickingImage = false; // <-- ADDED: State variable from in-situ
|
bool _isPickingImage = false;
|
||||||
|
|
||||||
late final TextEditingController _eventRemarksController;
|
late final TextEditingController _eventRemarksController;
|
||||||
late final TextEditingController _labRemarksController;
|
late final TextEditingController _labRemarksController;
|
||||||
@ -50,7 +50,6 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: MODIFIED _setImage function (from in-situ) ---
|
|
||||||
/// Handles picking and processing an image using the dedicated service.
|
/// Handles picking and processing an image using the dedicated service.
|
||||||
void _setImage(Function(File?) setImageCallback, ImageSource source, String imageInfo, {required bool isRequired}) async {
|
void _setImage(Function(File?) setImageCallback, ImageSource source, String imageInfo, {required bool isRequired}) async {
|
||||||
if (_isPickingImage) return;
|
if (_isPickingImage) return;
|
||||||
@ -60,21 +59,19 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
|
|
||||||
// Always pass `isRequired: true` to the service to enforce landscape check
|
// Always pass `isRequired: true` to the service to enforce landscape check
|
||||||
// and watermarking for ALL photos (required or optional).
|
// and watermarking for ALL photos (required or optional).
|
||||||
// The 'isRequired' param is just for the UI text.
|
|
||||||
final file = await service.pickAndProcessImage(source, data: widget.data, imageInfo: imageInfo, isRequired: true);
|
final file = await service.pickAndProcessImage(source, data: widget.data, imageInfo: imageInfo, isRequired: true);
|
||||||
|
|
||||||
|
// --- FIX: Check if mounted after async call to prevent framework crash ---
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
setState(() => setImageCallback(file));
|
setState(() => setImageCallback(file));
|
||||||
} else if (mounted) {
|
} else {
|
||||||
// Corrected snackbar message
|
|
||||||
_showSnackBar('Image selection failed. Please ensure all photos are taken in landscape (horizontal) mode.', isError: true);
|
_showSnackBar('Image selection failed. Please ensure all photos are taken in landscape (horizontal) mode.', isError: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
setState(() => _isPickingImage = false);
|
||||||
setState(() => _isPickingImage = false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// --- END: MODIFIED _setImage function ---
|
|
||||||
|
|
||||||
/// Validates the form and all required images before proceeding.
|
/// Validates the form and all required images before proceeding.
|
||||||
void _goToNextStep() {
|
void _goToNextStep() {
|
||||||
@ -86,7 +83,6 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Form validation handles the conditional requirement for Event Remarks
|
|
||||||
if (!_formKey.currentState!.validate()) {
|
if (!_formKey.currentState!.validate()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -106,7 +102,6 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Logic to determine if Event Remarks are required
|
|
||||||
final bool areAdditionalPhotosAttached = widget.data.phPaperImage != null ||
|
final bool areAdditionalPhotosAttached = widget.data.phPaperImage != null ||
|
||||||
widget.data.optionalImage1 != null ||
|
widget.data.optionalImage1 != null ||
|
||||||
widget.data.optionalImage2 != null ||
|
widget.data.optionalImage2 != null ||
|
||||||
@ -118,7 +113,6 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
children: [
|
children: [
|
||||||
// --- Section: On-Site Information ---
|
|
||||||
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),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
@ -146,9 +140,7 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// --- Section: Required Photos ---
|
|
||||||
Text("Required Photos *", style: Theme.of(context).textTheme.titleLarge),
|
Text("Required Photos *", style: Theme.of(context).textTheme.titleLarge),
|
||||||
// MODIFIED: Matched in-situ text
|
|
||||||
const Text(
|
const Text(
|
||||||
"All photos must be in landscape (horizontal) orientation. A watermark will be applied automatically.",
|
"All photos must be in landscape (horizontal) orientation. A watermark will be applied automatically.",
|
||||||
style: TextStyle(color: Colors.grey)
|
style: TextStyle(color: Colors.grey)
|
||||||
@ -160,7 +152,6 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
_buildImagePicker('Seawater in Clear Glass Bottle', 'SEAWATER_COLOR', widget.data.seawaterColorImage, (file) => widget.data.seawaterColorImage = file, isRequired: true),
|
_buildImagePicker('Seawater in Clear Glass Bottle', 'SEAWATER_COLOR', widget.data.seawaterColorImage, (file) => widget.data.seawaterColorImage = file, isRequired: true),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// --- Section: Additional photos and conditional remarks ---
|
|
||||||
Text("Additional Photos & Remarks", style: Theme.of(context).textTheme.titleLarge),
|
Text("Additional Photos & Remarks", style: Theme.of(context).textTheme.titleLarge),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildImagePicker('Examine Preservative (pH paper)', 'PH_PAPER', widget.data.phPaperImage, (file) => widget.data.phPaperImage = file, isRequired: false),
|
_buildImagePicker('Examine Preservative (pH paper)', 'PH_PAPER', widget.data.phPaperImage, (file) => widget.data.phPaperImage = file, isRequired: false),
|
||||||
@ -172,7 +163,6 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
|
|
||||||
Text("Remarks", style: Theme.of(context).textTheme.titleLarge),
|
Text("Remarks", style: Theme.of(context).textTheme.titleLarge),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Event Remarks field is conditionally required
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _eventRemarksController,
|
controller: _eventRemarksController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@ -206,8 +196,6 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: MODIFIED _buildImagePicker (from in-situ) ---
|
|
||||||
/// A reusable widget for picking and displaying an image.
|
|
||||||
Widget _buildImagePicker(String title, String imageInfo, File? imageFile, Function(File?) setImageCallback, {bool isRequired = false}) {
|
Widget _buildImagePicker(String title, String imageInfo, File? imageFile, Function(File?) setImageCallback, {bool isRequired = false}) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
@ -223,7 +211,6 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
child: FutureBuilder<Uint8List>(
|
child: FutureBuilder<Uint8List>(
|
||||||
// Use ValueKey to ensure FutureBuilder refetches when the file path changes
|
|
||||||
key: ValueKey(imageFile.path),
|
key: ValueKey(imageFile.path),
|
||||||
future: imageFile.readAsBytes(),
|
future: imageFile.readAsBytes(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
@ -243,7 +230,6 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
child: const Icon(Icons.error, color: Colors.red, size: 40),
|
child: const Icon(Icons.error, color: Colors.red, size: 40),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Display the image from memory
|
|
||||||
return Image.memory(
|
return Image.memory(
|
||||||
snapshot.data!,
|
snapshot.data!,
|
||||||
height: 150,
|
height: 150,
|
||||||
@ -276,5 +262,4 @@ class _MarineInvesManualStep2SiteInfoState extends State<MarineInvesManualStep2S
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// --- END: MODIFIED _buildImagePicker ---
|
|
||||||
}
|
}
|
||||||
@ -9,8 +9,8 @@ import 'package:usb_serial/usb_serial.dart';
|
|||||||
import '../../../../auth_provider.dart';
|
import '../../../../auth_provider.dart';
|
||||||
import '../../../../models/marine_inves_manual_sampling_data.dart';
|
import '../../../../models/marine_inves_manual_sampling_data.dart';
|
||||||
import '../../../../services/marine_investigative_sampling_service.dart';
|
import '../../../../services/marine_investigative_sampling_service.dart';
|
||||||
import '../../../../bluetooth/bluetooth_manager.dart'; // For connection state enum
|
import '../../../../bluetooth/bluetooth_manager.dart';
|
||||||
import '../../../../serial/serial_manager.dart'; // For connection state enum
|
import '../../../../serial/serial_manager.dart';
|
||||||
import '../../../../bluetooth/widgets/bluetooth_device_list_dialog.dart';
|
import '../../../../bluetooth/widgets/bluetooth_device_list_dialog.dart';
|
||||||
import '../../../../serial/widget/serial_port_list_dialog.dart';
|
import '../../../../serial/widget/serial_port_list_dialog.dart';
|
||||||
|
|
||||||
@ -177,12 +177,16 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
Future<void> _handleConnectionAttempt(String type) async {
|
Future<void> _handleConnectionAttempt(String type) async {
|
||||||
final service = context.read<MarineInvestigativeSamplingService>();
|
final service = context.read<MarineInvestigativeSamplingService>();
|
||||||
final hasPermissions = await service.requestDevicePermissions();
|
final hasPermissions = await service.requestDevicePermissions();
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
if (!hasPermissions && mounted) {
|
if (!hasPermissions && mounted) {
|
||||||
_showSnackBar("Bluetooth & Location permissions are required to connect.", isError: true);
|
_showSnackBar("Bluetooth & Location permissions are required to connect.", isError: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_disconnectFromAll();
|
_disconnectFromAll();
|
||||||
await Future.delayed(const Duration(milliseconds: 250));
|
await Future.delayed(const Duration(milliseconds: 250));
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
final bool connectionSuccess = await _connectToDevice(type);
|
final bool connectionSuccess = await _connectToDevice(type);
|
||||||
if (connectionSuccess && mounted) {
|
if (connectionSuccess && mounted) {
|
||||||
_dataSubscription?.cancel();
|
_dataSubscription?.cancel();
|
||||||
@ -200,6 +204,7 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
try {
|
try {
|
||||||
if (type == 'bluetooth') {
|
if (type == 'bluetooth') {
|
||||||
final devices = await service.getPairedBluetoothDevices();
|
final devices = await service.getPairedBluetoothDevices();
|
||||||
|
if (!mounted) return false;
|
||||||
if (devices.isEmpty && mounted) {
|
if (devices.isEmpty && mounted) {
|
||||||
_showSnackBar('No paired Bluetooth devices found.', isError: true);
|
_showSnackBar('No paired Bluetooth devices found.', isError: true);
|
||||||
return false;
|
return false;
|
||||||
@ -211,6 +216,7 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
}
|
}
|
||||||
} else if (type == 'serial') {
|
} else if (type == 'serial') {
|
||||||
final devices = await service.getAvailableSerialDevices();
|
final devices = await service.getAvailableSerialDevices();
|
||||||
|
if (!mounted) return false;
|
||||||
if (devices.isEmpty && mounted) {
|
if (devices.isEmpty && mounted) {
|
||||||
_showSnackBar('No USB Serial devices found.', isError: true);
|
_showSnackBar('No USB Serial devices found.', isError: true);
|
||||||
return false;
|
return false;
|
||||||
@ -337,7 +343,6 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: MODIFIED _validateAndProceed ---
|
|
||||||
void _validateAndProceed() {
|
void _validateAndProceed() {
|
||||||
if (_isLockedOut) {
|
if (_isLockedOut) {
|
||||||
_showSnackBar("Please wait for the initial reading period to complete.", isError: true);
|
_showSnackBar("Please wait for the initial reading period to complete.", isError: true);
|
||||||
@ -356,8 +361,6 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
final currentReadings = _captureReadingsToMap();
|
final currentReadings = _captureReadingsToMap();
|
||||||
List<Map<String, dynamic>> outOfBoundsParams = [];
|
List<Map<String, dynamic>> outOfBoundsParams = [];
|
||||||
|
|
||||||
// --- NEW CONDITIONAL LOGIC ---
|
|
||||||
// Only check limits if it's a Manual Station
|
|
||||||
if (widget.data.stationTypeSelection == 'Existing Manual Station') {
|
if (widget.data.stationTypeSelection == 'Existing Manual Station') {
|
||||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||||
final marineLimits = authProvider.marineParameterLimits ?? [];
|
final marineLimits = authProvider.marineParameterLimits ?? [];
|
||||||
@ -367,12 +370,10 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
_outOfBoundsKeys = outOfBoundsParams.map((p) => _parameters.firstWhere((param) => param['label'] == p['label'])['key'] as String).toSet();
|
_outOfBoundsKeys = outOfBoundsParams.map((p) => _parameters.firstWhere((param) => param['label'] == p['label'])['key'] as String).toSet();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// If not a manual station, ensure any old highlights are cleared
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_outOfBoundsKeys.clear();
|
_outOfBoundsKeys.clear();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// --- END NEW CONDITIONAL LOGIC ---
|
|
||||||
|
|
||||||
if (outOfBoundsParams.isNotEmpty) {
|
if (outOfBoundsParams.isNotEmpty) {
|
||||||
_showParameterLimitDialog(outOfBoundsParams, currentReadings);
|
_showParameterLimitDialog(outOfBoundsParams, currentReadings);
|
||||||
@ -380,7 +381,6 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
_saveDataAndMoveOn(currentReadings);
|
_saveDataAndMoveOn(currentReadings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- END: MODIFIED _validateAndProceed ---
|
|
||||||
|
|
||||||
Map<String, double> _captureReadingsToMap() {
|
Map<String, double> _captureReadingsToMap() {
|
||||||
final Map<String, double> readings = {};
|
final Map<String, double> readings = {};
|
||||||
@ -394,15 +394,7 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
|
|
||||||
List<Map<String, dynamic>> _validateParameters(Map<String, double> readings, List<Map<String, dynamic>> limits) {
|
List<Map<String, dynamic>> _validateParameters(Map<String, double> readings, List<Map<String, dynamic>> limits) {
|
||||||
final List<Map<String, dynamic>> invalidParams = [];
|
final List<Map<String, dynamic>> invalidParams = [];
|
||||||
|
final dynamic stationId = widget.data.selectedStation?['station_id'] ?? widget.data.selectedStation?['man_station_id'];
|
||||||
int? stationId;
|
|
||||||
// This check is now redundant due to _validateAndProceed, but safe to keep
|
|
||||||
if (widget.data.stationTypeSelection == 'Existing Manual Station') {
|
|
||||||
stationId = widget.data.selectedStation?['station_id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint("--- Parameter Validation Start (Investigative) ---");
|
|
||||||
debugPrint("Selected Station ID: $stationId (from 'man_station_id')");
|
|
||||||
|
|
||||||
double? _parseLimitValue(dynamic value) {
|
double? _parseLimitValue(dynamic value) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
@ -417,23 +409,17 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
final limitName = _parameterKeyToLimitName[key];
|
final limitName = _parameterKeyToLimitName[key];
|
||||||
if (limitName == null) return;
|
if (limitName == null) return;
|
||||||
|
|
||||||
debugPrint("Checking parameter: '$limitName' (key: '$key')");
|
|
||||||
|
|
||||||
Map<String, dynamic> limitData = {};
|
Map<String, dynamic> limitData = {};
|
||||||
|
|
||||||
if (stationId != null) {
|
if (stationId != null) {
|
||||||
limitData = limits.firstWhere(
|
limitData = limits.firstWhere(
|
||||||
(l) => l['param_parameter_list'] == limitName && l['station_id']?.toString() == stationId.toString(),
|
(l) => l['param_parameter_list'] == limitName &&
|
||||||
|
(l['station_id']?.toString() == stationId.toString() ||
|
||||||
|
l['man_station_id']?.toString() == stationId.toString()),
|
||||||
orElse: () => {},
|
orElse: () => {},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (limitData.isNotEmpty) {
|
|
||||||
debugPrint(" > Found station-specific limit for Station ID $stationId: $limitData");
|
|
||||||
} else {
|
|
||||||
debugPrint(" > No station-specific limit found for Station ID $stationId. Skipping check for this parameter.");
|
|
||||||
}
|
|
||||||
|
|
||||||
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']);
|
||||||
@ -450,8 +436,6 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
debugPrint("--- Parameter Validation End ---");
|
|
||||||
|
|
||||||
return invalidParams;
|
return invalidParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -531,9 +515,9 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
if (_isLockedOut) {
|
if (_isLockedOut) {
|
||||||
_showSnackBar("Please wait for the initial reading period to complete.", isError: true);
|
_showSnackBar("Please wait for the initial reading period to complete.", isError: true);
|
||||||
return false; // Prevent back navigation
|
return false;
|
||||||
}
|
}
|
||||||
return true; // Allow back navigation
|
return true;
|
||||||
},
|
},
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@ -832,7 +816,7 @@ class _MarineInvesManualStep3DataCaptureState extends State<MarineInvesManualSte
|
|||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: Icon(_isAutoReading ? Icons.stop_circle_outlined : Icons.play_circle_outlined),
|
icon: Icon(_isAutoReading ? Icons.stop_circle_outlined : Icons.play_circle_outlined),
|
||||||
label: Text(_isAutoReading
|
label: Text(_isAutoReading
|
||||||
? (_isLockedOut ? 'Stop Reading ($_lockoutSecondsRemaining\s)' : 'Stop Reading')
|
? (_isLockedOut ? 'Stop Reading ($_lockoutSecondsRemaining\\s)' : 'Stop Reading')
|
||||||
: 'Start Reading'),
|
: 'Start Reading'),
|
||||||
onPressed: (_isAutoReading && _isLockedOut) ? null : () => _toggleAutoReading(type),
|
onPressed: (_isAutoReading && _isLockedOut) ? null : () => _toggleAutoReading(type),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|||||||
@ -178,14 +178,14 @@ class _MarineInvesManualStep4SummaryState extends State<MarineInvesManualStep4Su
|
|||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (BuildContext dialogContext) {
|
builder: (BuildContext dialogContext) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text("NPE Parameter Limit Detected"),
|
title: const Text("Notification Pollution Event (NPE 1) Limit Detected"),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'The following parameters have fallen under the Non-Permissible Event (NPE) limit:'),
|
'The following parameters have fallen under the Notification Pollution Event (NPE) limit:'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Table(
|
Table(
|
||||||
columnWidths: const {
|
columnWidths: const {
|
||||||
|
|||||||
@ -77,7 +77,7 @@ class _MarineManualPreDepartureChecklistScreenState
|
|||||||
// Iterate through the map structure to initialize data
|
// Iterate through the map structure to initialize data
|
||||||
_checklistSections.forEach((section, items) {
|
_checklistSections.forEach((section, items) {
|
||||||
for (var item in items) {
|
for (var item in items) {
|
||||||
_data.checklistItems[item] = true; // MODIFIED: Default to 'Yes'
|
_data.checklistItems[item] = false; // MODIFIED: Default to 'No' (false)
|
||||||
_data.remarks[item] = '';
|
_data.remarks[item] = '';
|
||||||
_remarksVisibility[item] = false;
|
_remarksVisibility[item] = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -128,15 +128,16 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final position = await service.getCurrentLocation();
|
final position = await service.getCurrentLocation();
|
||||||
if (mounted) {
|
// --- FIX: Check if mounted after async call ---
|
||||||
setState(() {
|
if (!mounted) return;
|
||||||
widget.data.currentLatitude = position.latitude.toString();
|
|
||||||
widget.data.currentLongitude = position.longitude.toString();
|
setState(() {
|
||||||
_currentLatController.text = widget.data.currentLatitude!;
|
widget.data.currentLatitude = position.latitude.toString();
|
||||||
_currentLonController.text = widget.data.currentLongitude!;
|
widget.data.currentLongitude = position.longitude.toString();
|
||||||
_calculateDistance();
|
_currentLatController.text = widget.data.currentLatitude!;
|
||||||
});
|
_currentLonController.text = widget.data.currentLongitude!;
|
||||||
}
|
_calculateDistance();
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if(mounted) {
|
if(mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to get location: $e')));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to get location: $e')));
|
||||||
@ -175,6 +176,7 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => const SimpleBarcodeScannerPage()),
|
MaterialPageRoute(builder: (context) => const SimpleBarcodeScannerPage()),
|
||||||
);
|
);
|
||||||
|
// --- FIX: Check if mounted after returning from a new route ---
|
||||||
if (result is String && result != '-1' && mounted) {
|
if (result is String && result != '-1' && mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.data.sampleIdCode = result;
|
widget.data.sampleIdCode = result;
|
||||||
@ -187,6 +189,7 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
Future<void> _findAndShowNearbyStations() async {
|
Future<void> _findAndShowNearbyStations() async {
|
||||||
if (widget.data.currentLatitude == null || widget.data.currentLatitude!.isEmpty) {
|
if (widget.data.currentLatitude == null || widget.data.currentLatitude!.isEmpty) {
|
||||||
await _getCurrentLocation();
|
await _getCurrentLocation();
|
||||||
|
// --- FIX: Ensure we are still active after location fetch ---
|
||||||
if (!mounted || widget.data.currentLatitude == null || widget.data.currentLatitude!.isEmpty) {
|
if (!mounted || widget.data.currentLatitude == null || widget.data.currentLatitude!.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -221,7 +224,8 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
builder: (context) => _NearbyStationsDialog(nearbyStations: nearbyStations),
|
builder: (context) => _NearbyStationsDialog(nearbyStations: nearbyStations),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedStation != null) {
|
// --- FIX: Verify mounted state after dialog closure ---
|
||||||
|
if (selectedStation != null && mounted) {
|
||||||
_updateFormWithSelectedStation(selectedStation);
|
_updateFormWithSelectedStation(selectedStation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -338,11 +342,14 @@ class _InSituStep1SamplingInfoState extends State<InSituStep1SamplingInfo> {
|
|||||||
child: const Text('Confirm'),
|
child: const Text('Confirm'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (dialogFormKey.currentState!.validate()) {
|
if (dialogFormKey.currentState!.validate()) {
|
||||||
setState(() {
|
// --- FIX: Ensure mounted check inside dialog action ---
|
||||||
widget.data.distanceDifferenceRemarks = remarkController.text;
|
if (mounted) {
|
||||||
});
|
setState(() {
|
||||||
Navigator.of(context).pop();
|
widget.data.distanceDifferenceRemarks = remarkController.text;
|
||||||
widget.onNext();
|
});
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
widget.onNext();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -609,5 +616,4 @@ class _NearbyStationsDialog extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- END: New Dialog Widget ---
|
|
||||||
@ -111,6 +111,7 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
// --- FIX: Check if mounted before performing logic or calling setState ---
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
// Use the member variable, not context
|
// Use the member variable, not context
|
||||||
final service = _samplingService;
|
final service = _samplingService;
|
||||||
@ -196,12 +197,16 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
Future<void> _handleConnectionAttempt(String type) async {
|
Future<void> _handleConnectionAttempt(String type) async {
|
||||||
final service = context.read<MarineInSituSamplingService>();
|
final service = context.read<MarineInSituSamplingService>();
|
||||||
final hasPermissions = await service.requestDevicePermissions();
|
final hasPermissions = await service.requestDevicePermissions();
|
||||||
|
if (!mounted) return; // --- FIX: Prevent context usage if unmounted ---
|
||||||
|
|
||||||
if (!hasPermissions && mounted) {
|
if (!hasPermissions && mounted) {
|
||||||
_showSnackBar("Bluetooth & Location permissions are required to connect.", isError: true);
|
_showSnackBar("Bluetooth & Location permissions are required to connect.", isError: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_disconnectFromAll();
|
_disconnectFromAll();
|
||||||
await Future.delayed(const Duration(milliseconds: 250));
|
await Future.delayed(const Duration(milliseconds: 250));
|
||||||
|
if (!mounted) return; // --- FIX ---
|
||||||
|
|
||||||
final bool connectionSuccess = await _connectToDevice(type);
|
final bool connectionSuccess = await _connectToDevice(type);
|
||||||
if (connectionSuccess && mounted) {
|
if (connectionSuccess && mounted) {
|
||||||
_dataSubscription?.cancel();
|
_dataSubscription?.cancel();
|
||||||
@ -219,6 +224,8 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
try {
|
try {
|
||||||
if (type == 'bluetooth') {
|
if (type == 'bluetooth') {
|
||||||
final devices = await service.getPairedBluetoothDevices();
|
final devices = await service.getPairedBluetoothDevices();
|
||||||
|
if (!mounted) return false; // --- FIX ---
|
||||||
|
|
||||||
if (devices.isEmpty && mounted) {
|
if (devices.isEmpty && mounted) {
|
||||||
_showSnackBar('No paired Bluetooth devices found.', isError: true);
|
_showSnackBar('No paired Bluetooth devices found.', isError: true);
|
||||||
return false;
|
return false;
|
||||||
@ -230,6 +237,8 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
}
|
}
|
||||||
} else if (type == 'serial') {
|
} else if (type == 'serial') {
|
||||||
final devices = await service.getAvailableSerialDevices();
|
final devices = await service.getAvailableSerialDevices();
|
||||||
|
if (!mounted) return false; // --- FIX ---
|
||||||
|
|
||||||
if (devices.isEmpty && mounted) {
|
if (devices.isEmpty && mounted) {
|
||||||
_showSnackBar('No USB Serial devices found.', isError: true);
|
_showSnackBar('No USB Serial devices found.', isError: true);
|
||||||
return false;
|
return false;
|
||||||
@ -409,20 +418,13 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
|
|
||||||
List<Map<String, dynamic>> _validateParameters(Map<String, double> readings, List<Map<String, dynamic>> limits) {
|
List<Map<String, dynamic>> _validateParameters(Map<String, double> readings, List<Map<String, dynamic>> limits) {
|
||||||
final List<Map<String, dynamic>> invalidParams = [];
|
final List<Map<String, dynamic>> invalidParams = [];
|
||||||
final int? stationId = widget.data.selectedStation?['station_id'];
|
// --- FIX: Try both station_id and man_station_id from the data object ---
|
||||||
|
final dynamic stationId = widget.data.selectedStation?['station_id'] ?? widget.data.selectedStation?['man_station_id'];
|
||||||
|
|
||||||
debugPrint("--- Parameter Validation Start ---");
|
debugPrint("--- Parameter Validation Start ---");
|
||||||
debugPrint("Selected Station ID: $stationId");
|
debugPrint("Selected Station ID: $stationId");
|
||||||
debugPrint("Total Marine Limits Loaded: ${limits.length}");
|
debugPrint("Total Marine Limits Loaded: ${limits.length}");
|
||||||
|
|
||||||
// --- START DEBUG: Add type inspection ---
|
|
||||||
if (limits.isNotEmpty && stationId != null) {
|
|
||||||
debugPrint("Inspecting the first loaded limit record: ${limits.first}");
|
|
||||||
debugPrint("Type of Selected Station ID ($stationId): ${stationId.runtimeType}");
|
|
||||||
debugPrint("Type of man_station_id in first limit record: ${limits.first['man_station_id']?.runtimeType}");
|
|
||||||
}
|
|
||||||
// --- END DEBUG ---
|
|
||||||
|
|
||||||
double? _parseLimitValue(dynamic value) {
|
double? _parseLimitValue(dynamic value) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
if (value is num) return value.toDouble();
|
if (value is num) return value.toDouble();
|
||||||
@ -441,21 +443,17 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
Map<String, dynamic> limitData = {};
|
Map<String, dynamic> limitData = {};
|
||||||
|
|
||||||
if (stationId != null) {
|
if (stationId != null) {
|
||||||
// --- START FIX: Use type-safe comparison ---
|
// --- START FIX: Use robust string comparison to handle data type differences ---
|
||||||
limitData = limits.firstWhere(
|
limitData = limits.firstWhere(
|
||||||
(l) => l['param_parameter_list'] == limitName && l['station_id']?.toString() == stationId.toString(),
|
(l) => l['param_parameter_list'] == limitName &&
|
||||||
|
(l['station_id']?.toString() == stationId.toString() || l['man_station_id']?.toString() == stationId.toString()),
|
||||||
orElse: () => {},
|
orElse: () => {},
|
||||||
);
|
);
|
||||||
// --- END FIX ---
|
// --- END FIX ---
|
||||||
}
|
}
|
||||||
|
|
||||||
if (limitData.isNotEmpty) {
|
if (limitData.isNotEmpty) {
|
||||||
debugPrint(" > Found station-specific limit for Station ID $stationId: $limitData");
|
debugPrint(" > Found station-specific limit: $limitData");
|
||||||
} else {
|
|
||||||
debugPrint(" > No station-specific limit found for Station ID $stationId. Skipping check for this parameter.");
|
|
||||||
}
|
|
||||||
|
|
||||||
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']);
|
||||||
|
|
||||||
@ -468,6 +466,8 @@ class _InSituStep3DataCaptureState extends State<InSituStep3DataCapture> with Wi
|
|||||||
'upper_limit': upperLimit,
|
'upper_limit': upperLimit,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint(" > No station-specific limit found for $limitName.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -83,11 +83,13 @@ class _InSituStep4SummaryState extends State<InSituStep4Summary> {
|
|||||||
final limitName = _parameterKeyToLimitName[key];
|
final limitName = _parameterKeyToLimitName[key];
|
||||||
if (limitName == null) return;
|
if (limitName == null) return;
|
||||||
|
|
||||||
// Find limits for this specific station
|
// ✅ FIX: Use robust string comparison to handle data type differences (int vs String)
|
||||||
|
// Checks both 'station_id' and 'man_station_id' keys in the limits list
|
||||||
final limitData = marineLimits.firstWhere(
|
final limitData = marineLimits.firstWhere(
|
||||||
(l) =>
|
(l) =>
|
||||||
l['param_parameter_list'] == limitName &&
|
l['param_parameter_list'] == limitName &&
|
||||||
l['station_id']?.toString() == stationId.toString(),
|
(l['station_id']?.toString() == stationId.toString() ||
|
||||||
|
l['man_station_id']?.toString() == stationId.toString()),
|
||||||
orElse: () => {},
|
orElse: () => {},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -179,14 +181,14 @@ class _InSituStep4SummaryState extends State<InSituStep4Summary> {
|
|||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (BuildContext dialogContext) {
|
builder: (BuildContext dialogContext) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text("NPE Parameter Limit Detected"),
|
title: const Text("Notification Pollution Event (NPE 1) Limit Detected"),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'The following parameters have fallen under the Non-Permissible Event (NPE) limit:'),
|
'The following parameters have fallen under the Notification Pollution Event (NPE) limit:'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Table(
|
Table(
|
||||||
columnWidths: const {
|
columnWidths: const {
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:simple_barcode_scanner/simple_barcode_scanner.dart';
|
import 'package:simple_barcode_scanner/simple_barcode_scanner.dart';
|
||||||
|
|
||||||
import '../../../../auth_provider.dart';
|
import '../../../../auth_provider.dart';
|
||||||
import '../../../../models/river_inves_manual_sampling_data.dart'; // Updated model
|
import '../../../../models/river_inves_manual_sampling_data.dart';
|
||||||
import '../../../../services/river_investigative_sampling_service.dart'; // Updated service
|
import '../../../../services/river_investigative_sampling_service.dart';
|
||||||
|
|
||||||
class RiverInvesStep1SamplingInfo extends StatefulWidget {
|
class RiverInvesStep1SamplingInfo extends StatefulWidget {
|
||||||
final RiverInvesManualSamplingData data;
|
final RiverInvesManualSamplingData data;
|
||||||
@ -29,6 +29,9 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
bool _isLoadingLocation = false;
|
bool _isLoadingLocation = false;
|
||||||
|
|
||||||
|
// New flag to track if user manually typed the location
|
||||||
|
bool _isManualLocationEntry = false;
|
||||||
|
|
||||||
late final TextEditingController _firstSamplerController;
|
late final TextEditingController _firstSamplerController;
|
||||||
late final TextEditingController _dateController;
|
late final TextEditingController _dateController;
|
||||||
late final TextEditingController _timeController;
|
late final TextEditingController _timeController;
|
||||||
@ -52,7 +55,6 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
'Existing Triennial Station',
|
'Existing Triennial Station',
|
||||||
'New Location'
|
'New Location'
|
||||||
];
|
];
|
||||||
// Note: Investigative sampling type is fixed in the model
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -109,9 +111,7 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
_dateController.text = widget.data.samplingDate!;
|
_dateController.text = widget.data.samplingDate!;
|
||||||
_timeController.text = widget.data.samplingTime!;
|
_timeController.text = widget.data.samplingTime!;
|
||||||
|
|
||||||
// Sampling type is fixed to Investigative in the model
|
// Populate states list logic (same as before)
|
||||||
|
|
||||||
// Populate states list from Manual stations (assuming they cover all states)
|
|
||||||
final allManualStations = auth.riverManualStations ?? [];
|
final allManualStations = auth.riverManualStations ?? [];
|
||||||
if (allManualStations.isNotEmpty) {
|
if (allManualStations.isNotEmpty) {
|
||||||
final states = allManualStations
|
final states = allManualStations
|
||||||
@ -124,18 +124,16 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
_statesList = states;
|
_statesList = states;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Fallback: If no manual stations, try getting states from Triennial or general States list
|
|
||||||
final allTriennialStations = auth.riverTriennialStations ?? [];
|
final allTriennialStations = auth.riverTriennialStations ?? [];
|
||||||
if (allTriennialStations.isNotEmpty) {
|
if (allTriennialStations.isNotEmpty) {
|
||||||
final states = allTriennialStations
|
final states = allTriennialStations
|
||||||
.map((s) => s['state_name'] as String?) // Assuming Triennial has state_name
|
.map((s) => s['state_name'] as String?)
|
||||||
.whereType<String>()
|
.whereType<String>()
|
||||||
.toSet()
|
.toSet()
|
||||||
.toList();
|
.toList();
|
||||||
states.sort();
|
states.sort();
|
||||||
setState(() { _statesList = states; });
|
setState(() { _statesList = states; });
|
||||||
} else {
|
} else {
|
||||||
// Further fallback
|
|
||||||
final generalStates = auth.states ?? [];
|
final generalStates = auth.states ?? [];
|
||||||
final states = generalStates
|
final states = generalStates
|
||||||
.map((s) => s['state_name'] as String?)
|
.map((s) => s['state_name'] as String?)
|
||||||
@ -147,10 +145,8 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Pre-load stations if state and type are already selected (e.g., coming back to step)
|
|
||||||
_loadStationsForSelectedState();
|
_loadStationsForSelectedState();
|
||||||
_calculateDistance(); // Recalculate distance on init
|
_calculateDistance();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _loadStationsForSelectedState() {
|
void _loadStationsForSelectedState() {
|
||||||
@ -168,7 +164,7 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
.compareTo(b['sampling_station_code'] ?? ''));
|
.compareTo(b['sampling_station_code'] ?? ''));
|
||||||
|
|
||||||
_triennialStationsForState = allTriennialStations
|
_triennialStationsForState = allTriennialStations
|
||||||
.where((s) => s['state_name'] == widget.data.selectedStateName) // Assuming Triennial has state_name
|
.where((s) => s['state_name'] == widget.data.selectedStateName)
|
||||||
.toList()
|
.toList()
|
||||||
..sort((a, b) => (a['triennial_station_code'] ?? '')
|
..sort((a, b) => (a['triennial_station_code'] ?? '')
|
||||||
.compareTo(b['triennial_station_code'] ?? ''));
|
.compareTo(b['triennial_station_code'] ?? ''));
|
||||||
@ -183,21 +179,23 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
final position = await service.getCurrentLocation();
|
final position = await service.getCurrentLocation();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
// Reset manual entry flag because we successfully used GPS
|
||||||
|
_isManualLocationEntry = false;
|
||||||
|
|
||||||
widget.data.currentLatitude = position.latitude.toString();
|
widget.data.currentLatitude = position.latitude.toString();
|
||||||
widget.data.currentLongitude = position.longitude.toString();
|
widget.data.currentLongitude = position.longitude.toString();
|
||||||
_currentLatController.text = widget.data.currentLatitude!;
|
_currentLatController.text = widget.data.currentLatitude!;
|
||||||
_currentLonController.text = widget.data.currentLongitude!;
|
_currentLonController.text = widget.data.currentLongitude!;
|
||||||
|
|
||||||
// --- MODIFICATION: Update station lat/lon ONLY if 'New Location' ---
|
// If 'New Location', update station lat/lon as well
|
||||||
if (widget.data.stationTypeSelection == 'New Location') {
|
if (widget.data.stationTypeSelection == 'New Location') {
|
||||||
widget.data.stationLatitude = position.latitude.toString();
|
widget.data.stationLatitude = position.latitude.toString();
|
||||||
widget.data.stationLongitude = position.longitude.toString();
|
widget.data.stationLongitude = position.longitude.toString();
|
||||||
_stationLatController.text = widget.data.stationLatitude!;
|
_stationLatController.text = widget.data.stationLatitude!;
|
||||||
_stationLonController.text = widget.data.stationLongitude!;
|
_stationLonController.text = widget.data.stationLongitude!;
|
||||||
}
|
}
|
||||||
// --- END MODIFICATION ---
|
|
||||||
|
|
||||||
_calculateDistance(); // Always calculate distance after getting current location
|
_calculateDistance();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -211,7 +209,6 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _calculateDistance() {
|
void _calculateDistance() {
|
||||||
final lat1Str = widget.data.stationLatitude;
|
final lat1Str = widget.data.stationLatitude;
|
||||||
final lon1Str = widget.data.stationLongitude;
|
final lon1Str = widget.data.stationLongitude;
|
||||||
@ -251,9 +248,7 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MODIFICATION: Disable Nearby Station for now, or adapt later ---
|
|
||||||
Future<void> _findAndShowNearbyStations() async {
|
Future<void> _findAndShowNearbyStations() async {
|
||||||
// Only works for Manual Stations currently
|
|
||||||
if (widget.data.stationTypeSelection != 'Existing Manual Station') {
|
if (widget.data.stationTypeSelection != 'Existing Manual Station') {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Nearby station search only available for Manual Stations.')));
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Nearby station search only available for Manual Stations.')));
|
||||||
return;
|
return;
|
||||||
@ -269,9 +264,12 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
final service = Provider.of<RiverInvestigativeSamplingService>(context, listen: false);
|
final service = Provider.of<RiverInvestigativeSamplingService>(context, listen: false);
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
|
||||||
final currentLat = double.parse(widget.data.currentLatitude!);
|
final currentLat = double.tryParse(widget.data.currentLatitude ?? '');
|
||||||
final currentLon = double.parse(widget.data.currentLongitude!);
|
final currentLon = double.tryParse(widget.data.currentLongitude ?? '');
|
||||||
final allStations = auth.riverManualStations ?? []; // Only search Manual
|
|
||||||
|
if (currentLat == null || currentLon == null) return;
|
||||||
|
|
||||||
|
final allStations = auth.riverManualStations ?? [];
|
||||||
final List<Map<String, dynamic>> nearbyStations = [];
|
final List<Map<String, dynamic>> nearbyStations = [];
|
||||||
|
|
||||||
for (var station in allStations) {
|
for (var station in allStations) {
|
||||||
@ -280,7 +278,7 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
|
|
||||||
if (stationLat is num && stationLon is num) {
|
if (stationLat is num && stationLon is num) {
|
||||||
final distance = service.calculateDistance(currentLat, currentLon, stationLat.toDouble(), stationLon.toDouble());
|
final distance = service.calculateDistance(currentLat, currentLon, stationLat.toDouble(), stationLon.toDouble());
|
||||||
if (distance <= 3.0) { // 3km radius
|
if (distance <= 3.0) {
|
||||||
nearbyStations.add({'station': station, 'distance': distance});
|
nearbyStations.add({'station': station, 'distance': distance});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -292,7 +290,7 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
|
|
||||||
final selectedStation = await showDialog<Map<String, dynamic>>(
|
final selectedStation = await showDialog<Map<String, dynamic>>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _NearbyStationsDialog(nearbyStations: nearbyStations), // Use the same dialog
|
builder: (context) => _NearbyStationsDialog(nearbyStations: nearbyStations),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedStation != null) {
|
if (selectedStation != null) {
|
||||||
@ -301,22 +299,20 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateFormWithSelectedManualStation(Map<String, dynamic> station) {
|
void _updateFormWithSelectedManualStation(Map<String, dynamic> station) {
|
||||||
// This specifically handles selecting a MANUAL station from nearby search or dropdown
|
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
final allManualStations = auth.riverManualStations ?? [];
|
final allManualStations = auth.riverManualStations ?? [];
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.data.stationTypeSelection = 'Existing Manual Station'; // Ensure type is correct
|
widget.data.stationTypeSelection = 'Existing Manual Station';
|
||||||
widget.data.selectedStateName = station['state_name'];
|
widget.data.selectedStateName = station['state_name'];
|
||||||
widget.data.selectedStation = station; // Set manual station
|
widget.data.selectedStation = station;
|
||||||
widget.data.selectedTriennialStation = null; // Clear triennial
|
widget.data.selectedTriennialStation = null;
|
||||||
_clearNewLocationFields(); // Clear new location fields
|
_clearNewLocationFields();
|
||||||
|
|
||||||
widget.data.stationLatitude = station['sampling_lat']?.toString();
|
widget.data.stationLatitude = station['sampling_lat']?.toString();
|
||||||
widget.data.stationLongitude = station['sampling_long']?.toString();
|
widget.data.stationLongitude = station['sampling_long']?.toString();
|
||||||
_stationLatController.text = widget.data.stationLatitude ?? '';
|
_stationLatController.text = widget.data.stationLatitude ?? '';
|
||||||
_stationLonController.text = widget.data.stationLongitude ?? '';
|
_stationLonController.text = widget.data.stationLongitude ?? '';
|
||||||
|
|
||||||
// Reload stations for the selected state if needed (mainly for UI consistency)
|
|
||||||
_manualStationsForState = allManualStations
|
_manualStationsForState = allManualStations
|
||||||
.where((s) => s['state_name'] == widget.data.selectedStateName)
|
.where((s) => s['state_name'] == widget.data.selectedStateName)
|
||||||
.toList()
|
.toList()
|
||||||
@ -327,22 +323,20 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateFormWithSelectedTriennialStation(Map<String, dynamic> station) {
|
void _updateFormWithSelectedTriennialStation(Map<String, dynamic> station) {
|
||||||
// This specifically handles selecting a TRIENNIAL station from dropdown
|
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
final allTriennialStations = auth.riverTriennialStations ?? [];
|
final allTriennialStations = auth.riverTriennialStations ?? [];
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.data.stationTypeSelection = 'Existing Triennial Station';
|
widget.data.stationTypeSelection = 'Existing Triennial Station';
|
||||||
widget.data.selectedStateName = station['state_name']; // Use state from Triennial data
|
widget.data.selectedStateName = station['state_name'];
|
||||||
widget.data.selectedTriennialStation = station; // Set triennial station
|
widget.data.selectedTriennialStation = station;
|
||||||
widget.data.selectedStation = null; // Clear manual
|
widget.data.selectedStation = null;
|
||||||
_clearNewLocationFields();
|
_clearNewLocationFields();
|
||||||
|
|
||||||
widget.data.stationLatitude = station['triennial_lat']?.toString(); // Use triennial keys
|
widget.data.stationLatitude = station['triennial_lat']?.toString();
|
||||||
widget.data.stationLongitude = station['triennial_long']?.toString(); // Use triennial keys
|
widget.data.stationLongitude = station['triennial_long']?.toString();
|
||||||
_stationLatController.text = widget.data.stationLatitude ?? '';
|
_stationLatController.text = widget.data.stationLatitude ?? '';
|
||||||
_stationLonController.text = widget.data.stationLongitude ?? '';
|
_stationLonController.text = widget.data.stationLongitude ?? '';
|
||||||
|
|
||||||
// Reload stations for state (UI consistency)
|
|
||||||
_triennialStationsForState = allTriennialStations
|
_triennialStationsForState = allTriennialStations
|
||||||
.where((s) => s['state_name'] == widget.data.selectedStateName)
|
.where((s) => s['state_name'] == widget.data.selectedStateName)
|
||||||
.toList()
|
.toList()
|
||||||
@ -371,15 +365,13 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
_newBasinController.clear();
|
_newBasinController.clear();
|
||||||
_newRiverController.clear();
|
_newRiverController.clear();
|
||||||
_newStationCodeController.clear();
|
_newStationCodeController.clear();
|
||||||
// Don't clear station lat/lon here, as they might be set by GPS for new location
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _goToNextStep() {
|
Future<void> _goToNextStep() async {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
_formKey.currentState!.save(); // Save form fields to widget.data
|
_formKey.currentState!.save();
|
||||||
|
|
||||||
// --- Additional Validation for New Location ---
|
|
||||||
if (widget.data.stationTypeSelection == 'New Location') {
|
if (widget.data.stationTypeSelection == 'New Location') {
|
||||||
if (widget.data.stationLatitude == null || widget.data.stationLatitude!.isEmpty ||
|
if (widget.data.stationLatitude == null || widget.data.stationLatitude!.isEmpty ||
|
||||||
widget.data.stationLongitude == null || widget.data.stationLongitude!.isEmpty ) {
|
widget.data.stationLongitude == null || widget.data.stationLongitude!.isEmpty ) {
|
||||||
@ -389,20 +381,58 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- End Additional Validation ---
|
|
||||||
|
// NEW: Check if manually entered, ask for confirmation
|
||||||
|
if (_isManualLocationEntry) {
|
||||||
|
final confirmed = await _showLocationConfirmationDialog();
|
||||||
|
if (confirmed != true) {
|
||||||
|
return; // Stop if user cancels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final distanceInMeters = (widget.data.distanceDifferenceInKm ?? 0) * 1000;
|
final distanceInMeters = (widget.data.distanceDifferenceInKm ?? 0) * 1000;
|
||||||
|
|
||||||
// Only show distance warning if NOT a new location and distance > 50m
|
|
||||||
if (widget.data.stationTypeSelection != 'New Location' && distanceInMeters > 50) {
|
if (widget.data.stationTypeSelection != 'New Location' && distanceInMeters > 50) {
|
||||||
_showDistanceRemarkDialog();
|
_showDistanceRemarkDialog();
|
||||||
} else {
|
} else {
|
||||||
widget.data.distanceDifferenceRemarks = null; // Clear remark if not needed
|
widget.data.distanceDifferenceRemarks = null;
|
||||||
widget.onNext();
|
widget.onNext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEW: Dialog for manual location verification
|
||||||
|
Future<bool?> _showLocationConfirmationDialog() {
|
||||||
|
return showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Verify Coordinates'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('You have manually entered the location. Please confirm these coordinates are correct:'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text('Latitude: ${widget.data.currentLatitude}', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
Text('Longitude: ${widget.data.currentLongitude}', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Edit'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Confirm'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _showDistanceRemarkDialog() async {
|
Future<void> _showDistanceRemarkDialog() async {
|
||||||
final remarkController = TextEditingController(text: widget.data.distanceDifferenceRemarks);
|
final remarkController = TextEditingController(text: widget.data.distanceDifferenceRemarks);
|
||||||
final dialogFormKey = GlobalKey<FormState>();
|
final dialogFormKey = GlobalKey<FormState>();
|
||||||
@ -456,7 +486,7 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
widget.data.distanceDifferenceRemarks = remarkController.text;
|
widget.data.distanceDifferenceRemarks = remarkController.text;
|
||||||
});
|
});
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
widget.onNext(); // Proceed after confirming remark
|
widget.onNext();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -469,7 +499,6 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
// Note: Station lists (_manualStationsForState, _triennialStationsForState) are updated in callbacks
|
|
||||||
final allUsers = auth.allUsers ?? [];
|
final allUsers = auth.allUsers ?? [];
|
||||||
|
|
||||||
final secondSamplersList = allUsers
|
final secondSamplersList = allUsers
|
||||||
@ -526,7 +555,6 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Sampling Type is fixed for Investigative
|
|
||||||
|
|
||||||
// --- Sample ID ---
|
// --- Sample ID ---
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@ -541,11 +569,11 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
validator: (val) =>
|
validator: (val) =>
|
||||||
val == null || val.isEmpty ? "Sample ID is required" : null,
|
val == null || val.isEmpty ? "Sample ID is required" : null,
|
||||||
onSaved: (val) => widget.data.sampleIdCode = val,
|
onSaved: (val) => widget.data.sampleIdCode = val,
|
||||||
onChanged: (val) => widget.data.sampleIdCode = val, // Update model immediately
|
onChanged: (val) => widget.data.sampleIdCode = val,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// --- NEW: Station Type Selection ---
|
// --- Station Type Selection ---
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: widget.data.stationTypeSelection,
|
value: widget.data.stationTypeSelection,
|
||||||
items: _stationTypes
|
items: _stationTypes
|
||||||
@ -556,14 +584,13 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
widget.data.stationTypeSelection = value;
|
widget.data.stationTypeSelection = value;
|
||||||
_clearStationSelections();
|
_clearStationSelections();
|
||||||
_clearNewLocationFields();
|
_clearNewLocationFields();
|
||||||
// If selecting New Location, prepopulate station coords with current if available
|
|
||||||
if (value == 'New Location' && widget.data.currentLatitude != null) {
|
if (value == 'New Location' && widget.data.currentLatitude != null) {
|
||||||
widget.data.stationLatitude = widget.data.currentLatitude;
|
widget.data.stationLatitude = widget.data.currentLatitude;
|
||||||
widget.data.stationLongitude = widget.data.currentLongitude;
|
widget.data.stationLongitude = widget.data.currentLongitude;
|
||||||
_stationLatController.text = widget.data.stationLatitude!;
|
_stationLatController.text = widget.data.stationLatitude!;
|
||||||
_stationLonController.text = widget.data.stationLongitude!;
|
_stationLonController.text = widget.data.stationLongitude!;
|
||||||
}
|
}
|
||||||
_calculateDistance(); // Recalculate distance
|
_calculateDistance();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
decoration: const InputDecoration(labelText: 'Station Type *'),
|
decoration: const InputDecoration(labelText: 'Station Type *'),
|
||||||
@ -588,7 +615,7 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
onChanged: (state) {
|
onChanged: (state) {
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.data.selectedStateName = state;
|
widget.data.selectedStateName = state;
|
||||||
_clearStationSelections(); // Clear selections when state changes
|
_clearStationSelections();
|
||||||
_loadStationsForSelectedState();
|
_loadStationsForSelectedState();
|
||||||
_calculateDistance();
|
_calculateDistance();
|
||||||
});
|
});
|
||||||
@ -632,7 +659,7 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
|
|
||||||
// == Existing Triennial Station ==
|
// == Existing Triennial Station ==
|
||||||
if (widget.data.stationTypeSelection == 'Existing Triennial Station') ...[
|
if (widget.data.stationTypeSelection == 'Existing Triennial Station') ...[
|
||||||
DropdownSearch<String>( // State selection might be needed if not pre-selected
|
DropdownSearch<String>(
|
||||||
items: _statesList,
|
items: _statesList,
|
||||||
selectedItem: widget.data.selectedStateName,
|
selectedItem: widget.data.selectedStateName,
|
||||||
popupProps: const PopupProps.menu(showSearchBox: true, searchFieldProps: TextFieldProps(decoration: InputDecoration(hintText: "Search State..."))),
|
popupProps: const PopupProps.menu(showSearchBox: true, searchFieldProps: TextFieldProps(decoration: InputDecoration(hintText: "Search State..."))),
|
||||||
@ -641,7 +668,7 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
setState(() {
|
setState(() {
|
||||||
widget.data.selectedStateName = state;
|
widget.data.selectedStateName = state;
|
||||||
_clearStationSelections();
|
_clearStationSelections();
|
||||||
_loadStationsForSelectedState(); // Reloads both manual and triennial lists
|
_loadStationsForSelectedState();
|
||||||
_calculateDistance();
|
_calculateDistance();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -653,7 +680,7 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
selectedItem: widget.data.selectedTriennialStation,
|
selectedItem: widget.data.selectedTriennialStation,
|
||||||
enabled: widget.data.selectedStateName != null,
|
enabled: widget.data.selectedStateName != null,
|
||||||
itemAsString: (station) =>
|
itemAsString: (station) =>
|
||||||
"${station['triennial_station_code']} | ${station['triennial_river']} | ${station['triennial_basin']}", // Use triennial keys
|
"${station['triennial_station_code']} | ${station['triennial_river']} | ${station['triennial_basin']}",
|
||||||
popupProps: const PopupProps.menu(
|
popupProps: const PopupProps.menu(
|
||||||
showSearchBox: true,
|
showSearchBox: true,
|
||||||
searchFieldProps: TextFieldProps(
|
searchFieldProps: TextFieldProps(
|
||||||
@ -675,7 +702,7 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
|
|
||||||
// == New Location ==
|
// == New Location ==
|
||||||
if (widget.data.stationTypeSelection == 'New Location') ...[
|
if (widget.data.stationTypeSelection == 'New Location') ...[
|
||||||
DropdownSearch<String>( // Use Dropdown for State consistency
|
DropdownSearch<String>(
|
||||||
items: _statesList,
|
items: _statesList,
|
||||||
selectedItem: widget.data.newStateName,
|
selectedItem: widget.data.newStateName,
|
||||||
popupProps: const PopupProps.menu(showSearchBox: true, searchFieldProps: TextFieldProps(decoration: InputDecoration(hintText: "Search State..."))),
|
popupProps: const PopupProps.menu(showSearchBox: true, searchFieldProps: TextFieldProps(decoration: InputDecoration(hintText: "Search State..."))),
|
||||||
@ -683,7 +710,7 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
onChanged: (state) {
|
onChanged: (state) {
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.data.newStateName = state;
|
widget.data.newStateName = state;
|
||||||
widget.data.selectedStateName = state; // Keep consistent if needed elsewhere
|
widget.data.selectedStateName = state;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
validator: (val) => val == null ? "State is required" : null,
|
validator: (val) => val == null ? "State is required" : null,
|
||||||
@ -708,7 +735,7 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
onChanged: (val) => widget.data.newRiverName = val,
|
onChanged: (val) => widget.data.newRiverName = val,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField( // Optional Station Code for New Location
|
TextFormField(
|
||||||
controller: _newStationCodeController,
|
controller: _newStationCodeController,
|
||||||
decoration: const InputDecoration(labelText: 'Station Code (Optional)'),
|
decoration: const InputDecoration(labelText: 'Station Code (Optional)'),
|
||||||
onSaved: (val) => widget.data.newStationCode = val,
|
onSaved: (val) => widget.data.newStationCode = val,
|
||||||
@ -717,20 +744,20 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
],
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// --- Station Coordinates (Read-only for existing, editable/GPS-fed for new) ---
|
// --- Station Coordinates ---
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _stationLatController,
|
controller: _stationLatController,
|
||||||
readOnly: !isNewLocation, // Editable only for New Location
|
readOnly: !isNewLocation,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Station Latitude ${isNewLocation ? "*" : ""}',
|
labelText: 'Station Latitude ${isNewLocation ? "*" : ""}',
|
||||||
hintText: isNewLocation ? 'Use GPS or enter manually' : null
|
hintText: isNewLocation ? 'Use GPS or enter manually' : null
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||||
validator: (val) => isNewLocation && (val == null || val.isEmpty) ? "Latitude is required for new location" : null,
|
validator: (val) => isNewLocation && (val == null || val.isEmpty) ? "Latitude is required for new location" : null,
|
||||||
onChanged: (val) { // Allow manual edit for New Location
|
onChanged: (val) {
|
||||||
if (isNewLocation) {
|
if (isNewLocation) {
|
||||||
widget.data.stationLatitude = val;
|
widget.data.stationLatitude = val;
|
||||||
_calculateDistance(); // Recalculate if manually changed
|
_calculateDistance();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSaved: (val) => widget.data.stationLatitude = val,
|
onSaved: (val) => widget.data.stationLatitude = val,
|
||||||
@ -738,17 +765,17 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _stationLonController,
|
controller: _stationLonController,
|
||||||
readOnly: !isNewLocation, // Editable only for New Location
|
readOnly: !isNewLocation,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Station Longitude ${isNewLocation ? "*" : ""}',
|
labelText: 'Station Longitude ${isNewLocation ? "*" : ""}',
|
||||||
hintText: isNewLocation ? 'Use GPS or enter manually' : null
|
hintText: isNewLocation ? 'Use GPS or enter manually' : null
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||||
validator: (val) => isNewLocation && (val == null || val.isEmpty) ? "Longitude is required for new location" : null,
|
validator: (val) => isNewLocation && (val == null || val.isEmpty) ? "Longitude is required for new location" : null,
|
||||||
onChanged: (val) { // Allow manual edit for New Location
|
onChanged: (val) {
|
||||||
if (isNewLocation) {
|
if (isNewLocation) {
|
||||||
widget.data.stationLongitude = val;
|
widget.data.stationLongitude = val;
|
||||||
_calculateDistance(); // Recalculate if manually changed
|
_calculateDistance();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSaved: (val) => widget.data.stationLongitude = val,
|
onSaved: (val) => widget.data.stationLongitude = val,
|
||||||
@ -760,16 +787,41 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
Text("Location Verification",
|
Text("Location Verification",
|
||||||
style: Theme.of(context).textTheme.titleLarge),
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// MODIFIED: Enabled manual entry for Current Latitude
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _currentLatController,
|
controller: _currentLatController,
|
||||||
readOnly: true,
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
decoration: const InputDecoration(labelText: 'Current Latitude')),
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Current Latitude *',
|
||||||
|
hintText: 'e.g. 3.14159',
|
||||||
|
),
|
||||||
|
validator: (val) => val == null || val.isEmpty ? "Latitude is required" : null,
|
||||||
|
onChanged: (val) {
|
||||||
|
_isManualLocationEntry = true;
|
||||||
|
widget.data.currentLatitude = val;
|
||||||
|
_calculateDistance();
|
||||||
|
},
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// MODIFIED: Enabled manual entry for Current Longitude
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _currentLonController,
|
controller: _currentLonController,
|
||||||
readOnly: true,
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
decoration: const InputDecoration(labelText: 'Current Longitude')),
|
decoration: const InputDecoration(
|
||||||
if (widget.data.distanceDifferenceInKm != null && widget.data.stationTypeSelection != 'New Location') // Only show distance if NOT new location
|
labelText: 'Current Longitude *',
|
||||||
|
hintText: 'e.g. 101.68685',
|
||||||
|
),
|
||||||
|
validator: (val) => val == null || val.isEmpty ? "Longitude is required" : null,
|
||||||
|
onChanged: (val) {
|
||||||
|
_isManualLocationEntry = true;
|
||||||
|
widget.data.currentLongitude = val;
|
||||||
|
_calculateDistance();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
if (widget.data.distanceDifferenceInKm != null && widget.data.stationTypeSelection != 'New Location')
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -813,7 +865,7 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
height: 20,
|
height: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2))
|
child: CircularProgressIndicator(strokeWidth: 2))
|
||||||
: const Icon(Icons.location_searching),
|
: const Icon(Icons.location_searching),
|
||||||
label: Text(isNewLocation ? "Get Current Location (for Station & Verification)" : "Get Current Location"),
|
label: Text(isNewLocation ? "Get Current Location (GPS)" : "Get Current Location (GPS)"),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
@ -830,8 +882,6 @@ class _RiverInvesStep1SamplingInfoState extends State<RiverInvesStep1SamplingInf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Re-use the same dialog as River Manual In-Situ for nearby stations
|
|
||||||
class _NearbyStationsDialog extends StatelessWidget {
|
class _NearbyStationsDialog extends StatelessWidget {
|
||||||
final List<Map<String, dynamic>> nearbyStations;
|
final List<Map<String, dynamic>> nearbyStations;
|
||||||
|
|
||||||
@ -859,7 +909,7 @@ class _NearbyStationsDialog extends StatelessWidget {
|
|||||||
subtitle: Text("${station['sampling_river'] ?? 'N/A'}"),
|
subtitle: Text("${station['sampling_river'] ?? 'N/A'}"),
|
||||||
trailing: Text("${distanceInMeters.toStringAsFixed(0)} m"),
|
trailing: Text("${distanceInMeters.toStringAsFixed(0)} m"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop(station); // Return the selected station map
|
Navigator.of(context).pop(station);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -28,6 +28,9 @@ class _RiverManualTriennialStep1SamplingInfoState extends State<RiverManualTrien
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
bool _isLoadingLocation = false;
|
bool _isLoadingLocation = false;
|
||||||
|
|
||||||
|
// New flag to track if user manually typed the location
|
||||||
|
bool _isManualLocationEntry = false;
|
||||||
|
|
||||||
late final TextEditingController _firstSamplerController;
|
late final TextEditingController _firstSamplerController;
|
||||||
late final TextEditingController _dateController;
|
late final TextEditingController _dateController;
|
||||||
late final TextEditingController _timeController;
|
late final TextEditingController _timeController;
|
||||||
@ -117,6 +120,9 @@ class _RiverManualTriennialStep1SamplingInfoState extends State<RiverManualTrien
|
|||||||
final position = await service.getCurrentLocation();
|
final position = await service.getCurrentLocation();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
// Reset manual entry flag because we successfully used GPS
|
||||||
|
_isManualLocationEntry = false;
|
||||||
|
|
||||||
widget.data.currentLatitude = position.latitude.toString();
|
widget.data.currentLatitude = position.latitude.toString();
|
||||||
widget.data.currentLongitude = position.longitude.toString();
|
widget.data.currentLongitude = position.longitude.toString();
|
||||||
_currentLatController.text = widget.data.currentLatitude!;
|
_currentLatController.text = widget.data.currentLatitude!;
|
||||||
@ -153,6 +159,11 @@ class _RiverManualTriennialStep1SamplingInfoState extends State<RiverManualTrien
|
|||||||
setState(() {
|
setState(() {
|
||||||
widget.data.distanceDifferenceInKm = distance;
|
widget.data.distanceDifferenceInKm = distance;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Clear distance if parsing failed (e.g. user is typing)
|
||||||
|
setState(() {
|
||||||
|
widget.data.distanceDifferenceInKm = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,8 +192,11 @@ class _RiverManualTriennialStep1SamplingInfoState extends State<RiverManualTrien
|
|||||||
final service = Provider.of<RiverInSituSamplingService>(context, listen: false);
|
final service = Provider.of<RiverInSituSamplingService>(context, listen: false);
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
|
||||||
final currentLat = double.parse(widget.data.currentLatitude!);
|
final currentLat = double.tryParse(widget.data.currentLatitude ?? '');
|
||||||
final currentLon = double.parse(widget.data.currentLongitude!);
|
final currentLon = double.tryParse(widget.data.currentLongitude ?? '');
|
||||||
|
|
||||||
|
if (currentLat == null || currentLon == null) return;
|
||||||
|
|
||||||
final allStations = auth.riverManualStations ?? [];
|
final allStations = auth.riverManualStations ?? [];
|
||||||
final List<Map<String, dynamic>> nearbyStations = [];
|
final List<Map<String, dynamic>> nearbyStations = [];
|
||||||
|
|
||||||
@ -232,10 +246,18 @@ class _RiverManualTriennialStep1SamplingInfoState extends State<RiverManualTrien
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _goToNextStep() {
|
Future<void> _goToNextStep() async {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
_formKey.currentState!.save();
|
_formKey.currentState!.save();
|
||||||
|
|
||||||
|
// NEW: Check if manually entered, ask for confirmation
|
||||||
|
if (_isManualLocationEntry) {
|
||||||
|
final confirmed = await _showLocationConfirmationDialog();
|
||||||
|
if (confirmed != true) {
|
||||||
|
return; // Stop if user cancels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final distanceInMeters = (widget.data.distanceDifferenceInKm ?? 0) * 1000;
|
final distanceInMeters = (widget.data.distanceDifferenceInKm ?? 0) * 1000;
|
||||||
|
|
||||||
if (distanceInMeters > 50) {
|
if (distanceInMeters > 50) {
|
||||||
@ -247,6 +269,38 @@ class _RiverManualTriennialStep1SamplingInfoState extends State<RiverManualTrien
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEW: Dialog for manual location verification
|
||||||
|
Future<bool?> _showLocationConfirmationDialog() {
|
||||||
|
return showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Verify Coordinates'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('You have manually entered the location. Please confirm these coordinates are correct:'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text('Latitude: ${widget.data.currentLatitude}', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
Text('Longitude: ${widget.data.currentLongitude}', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Edit'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Confirm'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _showDistanceRemarkDialog() async {
|
Future<void> _showDistanceRemarkDialog() async {
|
||||||
final remarkController = TextEditingController(text: widget.data.distanceDifferenceRemarks);
|
final remarkController = TextEditingController(text: widget.data.distanceDifferenceRemarks);
|
||||||
final dialogFormKey = GlobalKey<FormState>();
|
final dialogFormKey = GlobalKey<FormState>();
|
||||||
@ -436,9 +490,42 @@ class _RiverManualTriennialStep1SamplingInfoState extends State<RiverManualTrien
|
|||||||
|
|
||||||
Text("Location Verification", style: Theme.of(context).textTheme.titleLarge),
|
Text("Location Verification", style: Theme.of(context).textTheme.titleLarge),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(controller: _currentLatController, readOnly: true, decoration: const InputDecoration(labelText: 'Current Latitude')),
|
|
||||||
|
// MODIFIED: Enabled manual entry for Current Latitude
|
||||||
|
TextFormField(
|
||||||
|
controller: _currentLatController,
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Current Latitude *',
|
||||||
|
hintText: 'e.g. 3.14159',
|
||||||
|
),
|
||||||
|
validator: (val) => val == null || val.isEmpty ? "Latitude is required" : null,
|
||||||
|
onChanged: (val) {
|
||||||
|
// Set manual flag to true and recalculate
|
||||||
|
_isManualLocationEntry = true;
|
||||||
|
widget.data.currentLatitude = val;
|
||||||
|
_calculateDistance();
|
||||||
|
},
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(controller: _currentLonController, readOnly: true, decoration: const InputDecoration(labelText: 'Current Longitude')),
|
|
||||||
|
// MODIFIED: Enabled manual entry for Current Longitude
|
||||||
|
TextFormField(
|
||||||
|
controller: _currentLonController,
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Current Longitude *',
|
||||||
|
hintText: 'e.g. 101.68685',
|
||||||
|
),
|
||||||
|
validator: (val) => val == null || val.isEmpty ? "Longitude is required" : null,
|
||||||
|
onChanged: (val) {
|
||||||
|
// Set manual flag to true and recalculate
|
||||||
|
_isManualLocationEntry = true;
|
||||||
|
widget.data.currentLongitude = val;
|
||||||
|
_calculateDistance();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
if (widget.data.distanceDifferenceInKm != null)
|
if (widget.data.distanceDifferenceInKm != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
@ -471,7 +558,7 @@ class _RiverManualTriennialStep1SamplingInfoState extends State<RiverManualTrien
|
|||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: _isLoadingLocation ? null : _getCurrentLocation,
|
onPressed: _isLoadingLocation ? null : _getCurrentLocation,
|
||||||
icon: _isLoadingLocation ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.location_searching),
|
icon: _isLoadingLocation ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.location_searching),
|
||||||
label: const Text("Get Current Location"),
|
label: const Text("Get Current Location (GPS)"),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
|||||||
@ -87,8 +87,12 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
final _flowrateValueController = TextEditingController();
|
final _flowrateValueController = TextEditingController();
|
||||||
final _sdHeightController = TextEditingController();
|
final _sdHeightController = TextEditingController();
|
||||||
final _sdDistanceController = TextEditingController();
|
final _sdDistanceController = TextEditingController();
|
||||||
final _sdTimeFirstController = TextEditingController();
|
|
||||||
final _sdTimeLastController = TextEditingController();
|
// --- MODIFICATION: Duration controllers instead of TimeFirst/Last ---
|
||||||
|
final _sdDurationHourController = TextEditingController();
|
||||||
|
final _sdDurationMinuteController = TextEditingController();
|
||||||
|
final _sdDurationSecondController = TextEditingController();
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -101,15 +105,9 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_dataSubscription?.cancel();
|
// --- MODIFICATION: Robust cleanup on dispose ---
|
||||||
_lockoutTimer?.cancel(); // --- MODIFICATION: Cancel timer on dispose ---
|
_disconnectFromAll();
|
||||||
|
// --- END MODIFICATION ---
|
||||||
if (_samplingService.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
|
||||||
_samplingService.disconnectFromBluetooth();
|
|
||||||
}
|
|
||||||
if (_samplingService.serialConnectionState.value != SerialConnectionState.disconnected) {
|
|
||||||
_samplingService.disconnectFromSerial();
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposeControllers();
|
_disposeControllers();
|
||||||
_disposeFlowrateControllers();
|
_disposeFlowrateControllers();
|
||||||
@ -140,6 +138,25 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- MODIFICATION: Added helper to clear data fields ---
|
||||||
|
void _clearDataFields() {
|
||||||
|
const defaultValue = '-999.0';
|
||||||
|
_oxyConcController.text = defaultValue;
|
||||||
|
_oxySatController.text = defaultValue;
|
||||||
|
_phController.text = defaultValue;
|
||||||
|
_salinityController.text = defaultValue;
|
||||||
|
_ecController.text = defaultValue;
|
||||||
|
_tempController.text = defaultValue;
|
||||||
|
_tdsController.text = defaultValue;
|
||||||
|
_turbidityController.text = defaultValue;
|
||||||
|
_ammoniaController.text = defaultValue;
|
||||||
|
_batteryController.text = defaultValue;
|
||||||
|
|
||||||
|
// Also clear Sonde ID
|
||||||
|
_sondeIdController.clear();
|
||||||
|
}
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
|
||||||
void _initializeControllers() {
|
void _initializeControllers() {
|
||||||
widget.data.dataCaptureDate = widget.data.samplingDate;
|
widget.data.dataCaptureDate = widget.data.samplingDate;
|
||||||
widget.data.dataCaptureTime = widget.data.samplingTime;
|
widget.data.dataCaptureTime = widget.data.samplingTime;
|
||||||
@ -196,16 +213,29 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
_flowrateValueController.text = widget.data.flowrateValue?.toString() ?? '';
|
_flowrateValueController.text = widget.data.flowrateValue?.toString() ?? '';
|
||||||
_sdHeightController.text = widget.data.flowrateSurfaceDrifterHeight?.toString() ?? '';
|
_sdHeightController.text = widget.data.flowrateSurfaceDrifterHeight?.toString() ?? '';
|
||||||
_sdDistanceController.text = widget.data.flowrateSurfaceDrifterDistance?.toString() ?? '';
|
_sdDistanceController.text = widget.data.flowrateSurfaceDrifterDistance?.toString() ?? '';
|
||||||
_sdTimeFirstController.text = widget.data.flowrateSurfaceDrifterTimeFirst ?? '';
|
|
||||||
_sdTimeLastController.text = widget.data.flowrateSurfaceDrifterTimeLast ?? '';
|
// --- MODIFICATION: Parse existing duration if available ---
|
||||||
|
if (widget.data.flowrateSurfaceDrifterTimeLast != null &&
|
||||||
|
widget.data.flowrateSurfaceDrifterTimeLast!.contains(':')) {
|
||||||
|
final parts = widget.data.flowrateSurfaceDrifterTimeLast!.split(':');
|
||||||
|
if (parts.length == 3) {
|
||||||
|
_sdDurationHourController.text = parts[0];
|
||||||
|
_sdDurationMinuteController.text = parts[1];
|
||||||
|
_sdDurationSecondController.text = parts[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END MODIFICATION ---
|
||||||
}
|
}
|
||||||
|
|
||||||
void _disposeFlowrateControllers() {
|
void _disposeFlowrateControllers() {
|
||||||
_flowrateValueController.dispose();
|
_flowrateValueController.dispose();
|
||||||
_sdHeightController.dispose();
|
_sdHeightController.dispose();
|
||||||
_sdDistanceController.dispose();
|
_sdDistanceController.dispose();
|
||||||
_sdTimeFirstController.dispose();
|
// --- MODIFICATION: Dispose duration controllers ---
|
||||||
_sdTimeLastController.dispose();
|
_sdDurationHourController.dispose();
|
||||||
|
_sdDurationMinuteController.dispose();
|
||||||
|
_sdDurationSecondController.dispose();
|
||||||
|
// --- END MODIFICATION ---
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onFlowrateMethodChanged(String? value) {
|
void _onFlowrateMethodChanged(String? value) {
|
||||||
@ -215,13 +245,14 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
if (value == 'NA') {
|
if (value == 'NA') {
|
||||||
_flowrateValueController.text = 'NA';
|
_flowrateValueController.text = 'NA';
|
||||||
} else if (value == 'Flowmeter') {
|
} else if (value == 'Flowmeter') {
|
||||||
// --- MODIFICATION: Clear flowrate value for Flowmeter ---
|
|
||||||
_flowrateValueController.clear();
|
_flowrateValueController.clear();
|
||||||
// --- END MODIFICATION ---
|
|
||||||
_sdHeightController.clear();
|
_sdHeightController.clear();
|
||||||
_sdDistanceController.clear();
|
_sdDistanceController.clear();
|
||||||
_sdTimeFirstController.clear();
|
// --- MODIFICATION: Clear duration fields ---
|
||||||
_sdTimeLastController.clear();
|
_sdDurationHourController.clear();
|
||||||
|
_sdDurationMinuteController.clear();
|
||||||
|
_sdDurationSecondController.clear();
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
|
||||||
} else { // Surface Drifter
|
} else { // Surface Drifter
|
||||||
_flowrateValueController.clear(); // Will be calculated
|
_flowrateValueController.clear(); // Will be calculated
|
||||||
@ -231,42 +262,32 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
|
|
||||||
void _calculateFlowrate() {
|
void _calculateFlowrate() {
|
||||||
final distance = double.tryParse(_sdDistanceController.text);
|
final distance = double.tryParse(_sdDistanceController.text);
|
||||||
final timeFirstStr = _sdTimeFirstController.text;
|
// --- MODIFICATION: Calculate using duration ---
|
||||||
final timeLastStr = _sdTimeLastController.text;
|
final hours = int.tryParse(_sdDurationHourController.text) ?? 0;
|
||||||
|
final minutes = int.tryParse(_sdDurationMinuteController.text) ?? 0;
|
||||||
|
final seconds = int.tryParse(_sdDurationSecondController.text) ?? 0;
|
||||||
|
|
||||||
if (distance == null || timeFirstStr.isEmpty || timeLastStr.isEmpty) {
|
if (distance == null) {
|
||||||
_showSnackBar("Please fill in Distance, Time First, and Time Last.", isError: true);
|
_showSnackBar("Please enter the Distance.", isError: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final totalSeconds = (hours * 3600) + (minutes * 60) + seconds;
|
||||||
|
|
||||||
|
if (totalSeconds <= 0) {
|
||||||
|
_showSnackBar("Total duration must be greater than zero.", isError: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final timeFormat = DateFormat("HH:mm:ss");
|
final flowrate = distance / totalSeconds;
|
||||||
// Use a common date (like today) to allow time difference calculation across midnight
|
|
||||||
final now = DateTime.now();
|
|
||||||
final timeFirst = timeFormat.parse(timeFirstStr);
|
|
||||||
final dateTimeFirst = DateTime(now.year, now.month, now.day, timeFirst.hour, timeFirst.minute, timeFirst.second);
|
|
||||||
|
|
||||||
final timeLast = timeFormat.parse(timeLastStr);
|
|
||||||
var dateTimeLast = DateTime(now.year, now.month, now.day, timeLast.hour, timeLast.minute, timeLast.second);
|
|
||||||
|
|
||||||
// Handle crossing midnight
|
|
||||||
if (dateTimeLast.isBefore(dateTimeFirst)) {
|
|
||||||
dateTimeLast = dateTimeLast.add(const Duration(days: 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
final differenceInSeconds = dateTimeLast.difference(dateTimeFirst).inSeconds;
|
|
||||||
|
|
||||||
if (differenceInSeconds <= 0) {
|
|
||||||
_showSnackBar("Time Last Deploy must be after Time First Deploy.", isError: true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final flowrate = distance / differenceInSeconds;
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_flowrateValueController.text = flowrate.toStringAsFixed(4);
|
_flowrateValueController.text = flowrate.toStringAsFixed(4);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_showSnackBar("Invalid time format. Please use HH:mm:ss.", isError: true);
|
_showSnackBar("Error calculating flowrate.", isError: true);
|
||||||
}
|
}
|
||||||
|
// --- END MODIFICATION ---
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _selectTime(BuildContext context, TextEditingController controller) async {
|
Future<void> _selectTime(BuildContext context, TextEditingController controller) async {
|
||||||
@ -290,8 +311,15 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
_showSnackBar("Bluetooth & Location permissions are required to connect.", isError: true);
|
_showSnackBar("Bluetooth & Location permissions are required to connect.", isError: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_disconnectFromAll();
|
|
||||||
await Future.delayed(const Duration(milliseconds: 250));
|
// --- MODIFICATION: Aggressive disconnect sequence ---
|
||||||
|
_disconnectFromAll(); // 1. Stop streams and disconnect services
|
||||||
|
_clearDataFields(); // 2. Clear all UI fields to remove old data
|
||||||
|
|
||||||
|
// 3. Wait to ensure streams are fully closed
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
|
||||||
final bool connectionSuccess = await _connectToDevice(type);
|
final bool connectionSuccess = await _connectToDevice(type);
|
||||||
if (connectionSuccess && mounted) {
|
if (connectionSuccess && mounted) {
|
||||||
_dataSubscription?.cancel();
|
_dataSubscription?.cancel();
|
||||||
@ -350,7 +378,7 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START MODIFICATION: Countdown Timer Logic ---
|
// --- START: MODIFIED VALIDATION FLOW ---
|
||||||
void _startLockoutTimer() {
|
void _startLockoutTimer() {
|
||||||
_lockoutTimer?.cancel();
|
_lockoutTimer?.cancel();
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -375,7 +403,6 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// --- END MODIFICATION ---
|
|
||||||
|
|
||||||
void _toggleAutoReading(String activeType) {
|
void _toggleAutoReading(String activeType) {
|
||||||
final service = context.read<RiverInSituSamplingService>();
|
final service = context.read<RiverInSituSamplingService>();
|
||||||
@ -400,9 +427,14 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
} else {
|
} else {
|
||||||
service.disconnectFromSerial();
|
service.disconnectFromSerial();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- MODIFICATION: Unconditional cleanup ---
|
||||||
_dataSubscription?.cancel();
|
_dataSubscription?.cancel();
|
||||||
_dataSubscription = null;
|
_dataSubscription = null;
|
||||||
_lockoutTimer?.cancel(); // --- MODIFICATION: Cancel timer on disconnect ---
|
_lockoutTimer?.cancel();
|
||||||
|
_clearDataFields(); // Clear UI
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isAutoReading = false;
|
_isAutoReading = false;
|
||||||
@ -413,15 +445,30 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _disconnectFromAll() {
|
void _disconnectFromAll() {
|
||||||
// --- START MODIFICATION ---
|
// --- MODIFICATION: Unconditional cleanup ---
|
||||||
final service = _samplingService; // NEW: Use the member variable
|
// 1. Cancel local listeners first
|
||||||
// --- END MODIFICATION ---
|
_dataSubscription?.cancel();
|
||||||
|
_dataSubscription = null;
|
||||||
|
_lockoutTimer?.cancel();
|
||||||
|
|
||||||
|
// 2. Disconnect services if active
|
||||||
|
final service = _samplingService;
|
||||||
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||||
_disconnect('bluetooth');
|
service.disconnectFromBluetooth();
|
||||||
}
|
}
|
||||||
if (service.serialConnectionState.value != SerialConnectionState.disconnected) {
|
if (service.serialConnectionState.value != SerialConnectionState.disconnected) {
|
||||||
_disconnect('serial');
|
service.disconnectFromSerial();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Reset local state
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isAutoReading = false;
|
||||||
|
_isLockedOut = false;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// --- END MODIFICATION ---
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateTextFields(Map<String, double> readings) {
|
void _updateTextFields(Map<String, double> readings) {
|
||||||
@ -440,7 +487,6 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: MODIFIED VALIDATION FLOW ---
|
|
||||||
void _validateAndProceed() async {
|
void _validateAndProceed() async {
|
||||||
// --- START MODIFICATION: Add lockout check ---
|
// --- START MODIFICATION: Add lockout check ---
|
||||||
if (_isLockedOut) {
|
if (_isLockedOut) {
|
||||||
@ -450,7 +496,6 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
// --- END MODIFICATION ---
|
// --- END MODIFICATION ---
|
||||||
|
|
||||||
// --- START MODIFICATION: Disable Next if Connected and Auto Reading is active ---
|
// --- START MODIFICATION: Disable Next if Connected and Auto Reading is active ---
|
||||||
// Similar to River In-Situ, allow manual stop to re-enable 'Next'
|
|
||||||
if (_isAutoReading) {
|
if (_isAutoReading) {
|
||||||
_showStopReadingDialog();
|
_showStopReadingDialog();
|
||||||
return;
|
return;
|
||||||
@ -478,7 +523,6 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
_saveDataAndMoveOn(currentReadings);
|
_saveDataAndMoveOn(currentReadings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- END: MODIFIED VALIDATION FLOW ---
|
|
||||||
|
|
||||||
Map<String, double> _captureReadingsToMap() {
|
Map<String, double> _captureReadingsToMap() {
|
||||||
final Map<String, double> readings = {};
|
final Map<String, double> readings = {};
|
||||||
@ -532,6 +576,7 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
void _saveDataAndMoveOn(Map<String, double> readings) {
|
void _saveDataAndMoveOn(Map<String, double> readings) {
|
||||||
try {
|
try {
|
||||||
const defaultValue = -999.0;
|
const defaultValue = -999.0;
|
||||||
|
widget.data.sondeId = _sondeIdController.text; // Ensure ID is saved
|
||||||
widget.data.temperature = readings['temperature'] ?? defaultValue;
|
widget.data.temperature = readings['temperature'] ?? defaultValue;
|
||||||
widget.data.ph = readings['ph'] ?? defaultValue;
|
widget.data.ph = readings['ph'] ?? defaultValue;
|
||||||
widget.data.salinity = readings['salinity'] ?? defaultValue;
|
widget.data.salinity = readings['salinity'] ?? defaultValue;
|
||||||
@ -547,8 +592,18 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
if (_selectedFlowrateMethod == 'Surface Drifter') {
|
if (_selectedFlowrateMethod == 'Surface Drifter') {
|
||||||
widget.data.flowrateSurfaceDrifterHeight = double.tryParse(_sdHeightController.text);
|
widget.data.flowrateSurfaceDrifterHeight = double.tryParse(_sdHeightController.text);
|
||||||
widget.data.flowrateSurfaceDrifterDistance = double.tryParse(_sdDistanceController.text);
|
widget.data.flowrateSurfaceDrifterDistance = double.tryParse(_sdDistanceController.text);
|
||||||
widget.data.flowrateSurfaceDrifterTimeFirst = _sdTimeFirstController.text;
|
|
||||||
widget.data.flowrateSurfaceDrifterTimeLast = _sdTimeLastController.text;
|
// --- MODIFICATION: Save formatted duration ---
|
||||||
|
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
||||||
|
String formattedDuration =
|
||||||
|
"${twoDigits(int.tryParse(_sdDurationHourController.text) ?? 0)}:"
|
||||||
|
"${twoDigits(int.tryParse(_sdDurationMinuteController.text) ?? 0)}:"
|
||||||
|
"${twoDigits(int.tryParse(_sdDurationSecondController.text) ?? 0)}";
|
||||||
|
|
||||||
|
widget.data.flowrateSurfaceDrifterTimeFirst = "00:00:00";
|
||||||
|
widget.data.flowrateSurfaceDrifterTimeLast = formattedDuration;
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
|
||||||
widget.data.flowrateValue = double.tryParse(_flowrateValueController.text);
|
widget.data.flowrateValue = double.tryParse(_flowrateValueController.text);
|
||||||
} else if (_selectedFlowrateMethod == 'Flowmeter') {
|
} else if (_selectedFlowrateMethod == 'Flowmeter') {
|
||||||
widget.data.flowrateSurfaceDrifterHeight = null;
|
widget.data.flowrateSurfaceDrifterHeight = null;
|
||||||
@ -809,7 +864,9 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.link_off),
|
icon: const Icon(Icons.link_off),
|
||||||
label: const Text('Disconnect'),
|
label: const Text('Disconnect'),
|
||||||
onPressed: () => _disconnect(type),
|
// --- MODIFICATION: Disabled during lockout ---
|
||||||
|
onPressed: _isLockedOut ? null : () => _disconnect(type),
|
||||||
|
// --- END MODIFICATION ---
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -985,7 +1042,6 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated to include disable logic
|
|
||||||
Widget _buildFlowrateSection({bool isInputDisabled = false}) {
|
Widget _buildFlowrateSection({bool isInputDisabled = false}) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
@ -1012,14 +1068,12 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// Wrap content in AbsorbPointer and Opacity if connected
|
|
||||||
AbsorbPointer(
|
AbsorbPointer(
|
||||||
absorbing: isInputDisabled,
|
absorbing: isInputDisabled,
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: isInputDisabled ? 0.5 : 1.0,
|
opacity: isInputDisabled ? 0.5 : 1.0,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Replaced Row with Wrap to fix horizontal overflow for radio buttons
|
|
||||||
Wrap(
|
Wrap(
|
||||||
alignment: WrapAlignment.spaceAround,
|
alignment: WrapAlignment.spaceAround,
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
@ -1027,10 +1081,9 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
children: [
|
children: [
|
||||||
_buildFlowrateRadioButton("Surface Drifter"),
|
_buildFlowrateRadioButton("Surface Drifter"),
|
||||||
_buildFlowrateRadioButton("Flowmeter"),
|
_buildFlowrateRadioButton("Flowmeter"),
|
||||||
_buildFlowrateRadioButton("NA"), // Not Applicable
|
_buildFlowrateRadioButton("NA"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// Conditional fields based on selected method
|
|
||||||
if (_selectedFlowrateMethod == 'Surface Drifter')
|
if (_selectedFlowrateMethod == 'Surface Drifter')
|
||||||
_buildSurfaceDrifterFields(),
|
_buildSurfaceDrifterFields(),
|
||||||
if (_selectedFlowrateMethod == 'Flowmeter')
|
if (_selectedFlowrateMethod == 'Flowmeter')
|
||||||
@ -1058,7 +1111,7 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
overflow: TextOverflow.ellipsis, // Add ellipsis handling for safety
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -1068,12 +1121,12 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _sdHeightController,
|
controller: _sdHeightController,
|
||||||
decoration: const InputDecoration(labelText: 'Height (m)'),
|
decoration: const InputDecoration(labelText: 'Height (m)'),
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
// Add validation if needed
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@ -1083,21 +1136,51 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
validator: (v) => v == null || v.isEmpty ? 'Distance is required' : null,
|
validator: (v) => v == null || v.isEmpty ? 'Distance is required' : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
|
||||||
controller: _sdTimeFirstController,
|
// --- MODIFICATION: Duration Input Fields ---
|
||||||
decoration: const InputDecoration(labelText: 'Time First Deploy (HH:mm:ss) *', suffixIcon: Icon(Icons.timer)),
|
const Text("Duration of Travel", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
|
||||||
readOnly: true,
|
const SizedBox(height: 8),
|
||||||
onTap: () => _selectTime(context, _sdTimeFirstController),
|
Row(
|
||||||
validator: (v) => v == null || v.isEmpty ? 'Start time is required' : null,
|
children: [
|
||||||
),
|
Expanded(
|
||||||
const SizedBox(height: 16),
|
child: TextFormField(
|
||||||
TextFormField(
|
controller: _sdDurationHourController,
|
||||||
controller: _sdTimeLastController,
|
decoration: const InputDecoration(
|
||||||
decoration: const InputDecoration(labelText: 'Time Last Deploy (HH:mm:ss) *', suffixIcon: Icon(Icons.timer)),
|
labelText: 'Hours',
|
||||||
readOnly: true,
|
counterText: "",
|
||||||
onTap: () => _selectTime(context, _sdTimeLastController),
|
),
|
||||||
validator: (v) => v == null || v.isEmpty ? 'End time is required' : null,
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _sdDurationMinuteController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Minutes',
|
||||||
|
counterText: "",
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _sdDurationSecondController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Seconds',
|
||||||
|
counterText: "",
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _calculateFlowrate,
|
onPressed: _calculateFlowrate,
|
||||||
@ -1108,7 +1191,6 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
controller: _flowrateValueController,
|
controller: _flowrateValueController,
|
||||||
decoration: const InputDecoration(labelText: 'Calculated Flowrate (m/s)'),
|
decoration: const InputDecoration(labelText: 'Calculated Flowrate (m/s)'),
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
// Add validator if calculation must be done?
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -1128,7 +1210,6 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNAField() {
|
Widget _buildNAField() {
|
||||||
// Fix: Use controller to set value instead of initialValue to avoid conflict crash
|
|
||||||
if (_flowrateValueController.text != 'NA') {
|
if (_flowrateValueController.text != 'NA') {
|
||||||
_flowrateValueController.text = 'NA';
|
_flowrateValueController.text = 'NA';
|
||||||
}
|
}
|
||||||
@ -1138,10 +1219,8 @@ class _RiverManualTriennialStep3DataCaptureState extends State<RiverManualTrienn
|
|||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _flowrateValueController,
|
controller: _flowrateValueController,
|
||||||
decoration: const InputDecoration(labelText: 'Flowrate (m/s)'),
|
decoration: const InputDecoration(labelText: 'Flowrate (m/s)'),
|
||||||
// initialValue: 'NA', // Removed to fix AssertionError: initialValue == null || controller == null
|
readOnly: true,
|
||||||
readOnly: true, // Make it read-only
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} // End of State class
|
|
||||||
@ -28,6 +28,9 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
bool _isLoadingLocation = false;
|
bool _isLoadingLocation = false;
|
||||||
|
|
||||||
|
// New flag to track if user manually typed the location
|
||||||
|
bool _isManualLocationEntry = false;
|
||||||
|
|
||||||
late final TextEditingController _firstSamplerController;
|
late final TextEditingController _firstSamplerController;
|
||||||
late final TextEditingController _dateController;
|
late final TextEditingController _dateController;
|
||||||
late final TextEditingController _timeController;
|
late final TextEditingController _timeController;
|
||||||
@ -100,9 +103,7 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
_stationsForState = allStations
|
_stationsForState = allStations
|
||||||
.where((s) => s['state_name'] == widget.data.selectedStateName)
|
.where((s) => s['state_name'] == widget.data.selectedStateName)
|
||||||
.toList()
|
.toList()
|
||||||
// --- START MODIFICATION: Sort stations on initial load ---
|
|
||||||
..sort((a, b) => (a['sampling_station_code'] ?? '').compareTo(b['sampling_station_code'] ?? ''));
|
..sort((a, b) => (a['sampling_station_code'] ?? '').compareTo(b['sampling_station_code'] ?? ''));
|
||||||
// --- END MODIFICATION ---
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -117,15 +118,20 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final position = await service.getCurrentLocation();
|
final position = await service.getCurrentLocation();
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
// FIX: Check if widget is still mounted before calling setState to prevent defunct exception
|
||||||
widget.data.currentLatitude = position.latitude.toString();
|
if (!mounted) return;
|
||||||
widget.data.currentLongitude = position.longitude.toString();
|
|
||||||
_currentLatController.text = widget.data.currentLatitude!;
|
setState(() {
|
||||||
_currentLonController.text = widget.data.currentLongitude!;
|
// Reset manual entry flag because we successfully used GPS
|
||||||
_calculateDistance();
|
_isManualLocationEntry = false;
|
||||||
});
|
|
||||||
}
|
widget.data.currentLatitude = position.latitude.toString();
|
||||||
|
widget.data.currentLongitude = position.longitude.toString();
|
||||||
|
_currentLatController.text = widget.data.currentLatitude!;
|
||||||
|
_currentLonController.text = widget.data.currentLongitude!;
|
||||||
|
_calculateDistance();
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if(mounted) {
|
if(mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to get location: $e')));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to get location: $e')));
|
||||||
@ -152,9 +158,18 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
|
|
||||||
if (lat1 != null && lon1 != null && lat2 != null && lon2 != null) {
|
if (lat1 != null && lon1 != null && lat2 != null && lon2 != null) {
|
||||||
final distance = service.calculateDistance(lat1, lon1, lat2, lon2);
|
final distance = service.calculateDistance(lat1, lon1, lat2, lon2);
|
||||||
setState(() {
|
if (mounted) {
|
||||||
widget.data.distanceDifferenceInKm = distance;
|
setState(() {
|
||||||
});
|
widget.data.distanceDifferenceInKm = distance;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Clear distance if parsing failed
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
widget.data.distanceDifferenceInKm = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,8 +198,11 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
final service = Provider.of<RiverInSituSamplingService>(context, listen: false);
|
final service = Provider.of<RiverInSituSamplingService>(context, listen: false);
|
||||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||||
|
|
||||||
final currentLat = double.parse(widget.data.currentLatitude!);
|
final currentLat = double.tryParse(widget.data.currentLatitude ?? '');
|
||||||
final currentLon = double.parse(widget.data.currentLongitude!);
|
final currentLon = double.tryParse(widget.data.currentLongitude ?? '');
|
||||||
|
|
||||||
|
if (currentLat == null || currentLon == null) return;
|
||||||
|
|
||||||
final allStations = auth.riverManualStations ?? [];
|
final allStations = auth.riverManualStations ?? [];
|
||||||
final List<Map<String, dynamic>> nearbyStations = [];
|
final List<Map<String, dynamic>> nearbyStations = [];
|
||||||
|
|
||||||
@ -209,35 +227,45 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
builder: (context) => _NearbyStationsDialog(nearbyStations: nearbyStations),
|
builder: (context) => _NearbyStationsDialog(nearbyStations: nearbyStations),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedStation != null) {
|
if (selectedStation != null && mounted) {
|
||||||
_updateFormWithSelectedStation(selectedStation);
|
_updateFormWithSelectedStation(selectedStation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateFormWithSelectedStation(Map<String, dynamic> station) {
|
void _updateFormWithSelectedStation(Map<String, dynamic> station) {
|
||||||
final allStations = Provider.of<AuthProvider>(context, listen: false).riverManualStations ?? [];
|
final allStations = Provider.of<AuthProvider>(context, listen: false).riverManualStations ?? [];
|
||||||
setState(() {
|
if (mounted) {
|
||||||
widget.data.selectedStateName = station['state_name'];
|
setState(() {
|
||||||
_stationsForState = allStations
|
widget.data.selectedStateName = station['state_name'];
|
||||||
.where((s) => s['state_name'] == widget.data.selectedStateName)
|
_stationsForState = allStations
|
||||||
.toList()
|
.where((s) => s['state_name'] == widget.data.selectedStateName)
|
||||||
..sort((a, b) => (a['sampling_station_code'] ?? '').compareTo(b['sampling_station_code'] ?? ''));
|
.toList()
|
||||||
|
..sort((a, b) => (a['sampling_station_code'] ?? '').compareTo(b['sampling_station_code'] ?? ''));
|
||||||
|
|
||||||
widget.data.selectedStation = station;
|
widget.data.selectedStation = station;
|
||||||
widget.data.stationLatitude = station['sampling_lat']?.toString();
|
widget.data.stationLatitude = station['sampling_lat']?.toString();
|
||||||
widget.data.stationLongitude = station['sampling_long']?.toString();
|
widget.data.stationLongitude = station['sampling_long']?.toString();
|
||||||
_stationLatController.text = widget.data.stationLatitude ?? '';
|
_stationLatController.text = widget.data.stationLatitude ?? '';
|
||||||
_stationLonController.text = widget.data.stationLongitude ?? '';
|
_stationLonController.text = widget.data.stationLongitude ?? '';
|
||||||
|
|
||||||
_calculateDistance();
|
_calculateDistance();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _goToNextStep() {
|
Future<void> _goToNextStep() async {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
_formKey.currentState!.save();
|
_formKey.currentState!.save();
|
||||||
|
|
||||||
|
// NEW: Check if manually entered, ask for confirmation
|
||||||
|
if (_isManualLocationEntry) {
|
||||||
|
final confirmed = await _showLocationConfirmationDialog();
|
||||||
|
if (confirmed != true) {
|
||||||
|
return; // Stop if user cancels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final distanceInMeters = (widget.data.distanceDifferenceInKm ?? 0) * 1000;
|
final distanceInMeters = (widget.data.distanceDifferenceInKm ?? 0) * 1000;
|
||||||
|
|
||||||
if (distanceInMeters > 50) {
|
if (distanceInMeters > 50) {
|
||||||
@ -249,6 +277,38 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEW: Dialog for manual location verification
|
||||||
|
Future<bool?> _showLocationConfirmationDialog() {
|
||||||
|
return showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Verify Coordinates'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('You have manually entered the location. Please confirm these coordinates are correct:'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text('Latitude: ${widget.data.currentLatitude}', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
Text('Longitude: ${widget.data.currentLongitude}', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Edit'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Confirm'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _showDistanceRemarkDialog() async {
|
Future<void> _showDistanceRemarkDialog() async {
|
||||||
final remarkController = TextEditingController(text: widget.data.distanceDifferenceRemarks);
|
final remarkController = TextEditingController(text: widget.data.distanceDifferenceRemarks);
|
||||||
final dialogFormKey = GlobalKey<FormState>();
|
final dialogFormKey = GlobalKey<FormState>();
|
||||||
@ -298,9 +358,11 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
child: const Text('Confirm'),
|
child: const Text('Confirm'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (dialogFormKey.currentState!.validate()) {
|
if (dialogFormKey.currentState!.validate()) {
|
||||||
setState(() {
|
if (mounted) {
|
||||||
widget.data.distanceDifferenceRemarks = remarkController.text;
|
setState(() {
|
||||||
});
|
widget.data.distanceDifferenceRemarks = remarkController.text;
|
||||||
|
});
|
||||||
|
}
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
widget.onNext();
|
widget.onNext();
|
||||||
}
|
}
|
||||||
@ -318,10 +380,8 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
final allStations = auth.riverManualStations ?? [];
|
final allStations = auth.riverManualStations ?? [];
|
||||||
final allUsers = auth.allUsers ?? [];
|
final allUsers = auth.allUsers ?? [];
|
||||||
|
|
||||||
// --- START MODIFICATION: Sort 2nd sampler list alphabetically ---
|
|
||||||
final secondSamplersList = allUsers.where((user) => user['user_id'] != auth.profileData?['user_id']).toList()
|
final secondSamplersList = allUsers.where((user) => user['user_id'] != auth.profileData?['user_id']).toList()
|
||||||
..sort((a, b) => (a['first_name'] ?? '').compareTo(b['first_name'] ?? ''));
|
..sort((a, b) => (a['first_name'] ?? '').compareTo(b['first_name'] ?? ''));
|
||||||
// --- END MODIFICATION ---
|
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@ -390,9 +450,7 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
? (allStations
|
? (allStations
|
||||||
.where((s) => s['state_name'] == state)
|
.where((s) => s['state_name'] == state)
|
||||||
.toList()
|
.toList()
|
||||||
// --- START MODIFICATION: Sort stations when state changes ---
|
|
||||||
..sort((a, b) => (a['sampling_station_code'] ?? '').compareTo(b['sampling_station_code'] ?? '')))
|
..sort((a, b) => (a['sampling_station_code'] ?? '').compareTo(b['sampling_station_code'] ?? '')))
|
||||||
// --- END MODIFICATION ---
|
|
||||||
: [];
|
: [];
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -442,9 +500,42 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
|
|
||||||
Text("Location Verification", style: Theme.of(context).textTheme.titleLarge),
|
Text("Location Verification", style: Theme.of(context).textTheme.titleLarge),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(controller: _currentLatController, readOnly: true, decoration: const InputDecoration(labelText: 'Current Latitude')),
|
|
||||||
|
// MODIFIED: Enabled manual entry for Current Latitude
|
||||||
|
TextFormField(
|
||||||
|
controller: _currentLatController,
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Current Latitude *',
|
||||||
|
hintText: 'e.g. 3.14159',
|
||||||
|
),
|
||||||
|
validator: (val) => val == null || val.isEmpty ? "Latitude is required" : null,
|
||||||
|
onChanged: (val) {
|
||||||
|
// Set manual flag to true and recalculate
|
||||||
|
_isManualLocationEntry = true;
|
||||||
|
widget.data.currentLatitude = val;
|
||||||
|
_calculateDistance();
|
||||||
|
},
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(controller: _currentLonController, readOnly: true, decoration: const InputDecoration(labelText: 'Current Longitude')),
|
|
||||||
|
// MODIFIED: Enabled manual entry for Current Longitude
|
||||||
|
TextFormField(
|
||||||
|
controller: _currentLonController,
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Current Longitude *',
|
||||||
|
hintText: 'e.g. 101.68685',
|
||||||
|
),
|
||||||
|
validator: (val) => val == null || val.isEmpty ? "Longitude is required" : null,
|
||||||
|
onChanged: (val) {
|
||||||
|
// Set manual flag to true and recalculate
|
||||||
|
_isManualLocationEntry = true;
|
||||||
|
widget.data.currentLongitude = val;
|
||||||
|
_calculateDistance();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
if (widget.data.distanceDifferenceInKm != null)
|
if (widget.data.distanceDifferenceInKm != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
@ -477,7 +568,7 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: _isLoadingLocation ? null : _getCurrentLocation,
|
onPressed: _isLoadingLocation ? null : _getCurrentLocation,
|
||||||
icon: _isLoadingLocation ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.location_searching),
|
icon: _isLoadingLocation ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.location_searching),
|
||||||
label: const Text("Get Current Location"),
|
label: const Text("Get Current Location (GPS)"),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
|||||||
@ -46,6 +46,9 @@ class _RiverInSituStep2SiteInfoState extends State<RiverInSituStep2SiteInfo> {
|
|||||||
|
|
||||||
void _setImage(Function(File?) setImageCallback, ImageSource source, String imageInfo, {required bool isRequired}) async {
|
void _setImage(Function(File?) setImageCallback, ImageSource source, String imageInfo, {required bool isRequired}) async {
|
||||||
if (_isPickingImage) return;
|
if (_isPickingImage) return;
|
||||||
|
|
||||||
|
// Safety check before starting
|
||||||
|
if (!mounted) return;
|
||||||
setState(() => _isPickingImage = true);
|
setState(() => _isPickingImage = true);
|
||||||
|
|
||||||
final service = Provider.of<RiverInSituSamplingService>(context, listen: false);
|
final service = Provider.of<RiverInSituSamplingService>(context, listen: false);
|
||||||
@ -62,6 +65,10 @@ class _RiverInSituStep2SiteInfoState extends State<RiverInSituStep2SiteInfo> {
|
|||||||
stationCode: stationCode, // Pass the station code here
|
stationCode: stationCode, // Pass the station code here
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- CRITICAL FIX: Check if widget is still mounted after the await call ---
|
||||||
|
// This prevents "Looking up a deactivated widget's ancestor" if user closed camera/app.
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
setState(() => setImageCallback(file));
|
setState(() => setImageCallback(file));
|
||||||
} else if (mounted) {
|
} else if (mounted) {
|
||||||
@ -201,4 +208,4 @@ class _RiverInSituStep2SiteInfoState extends State<RiverInSituStep2SiteInfo> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9,7 +9,6 @@ import 'package:intl/intl.dart';
|
|||||||
|
|
||||||
import '../../../../../auth_provider.dart';
|
import '../../../../../auth_provider.dart';
|
||||||
import '../../../../../models/river_in_situ_sampling_data.dart';
|
import '../../../../../models/river_in_situ_sampling_data.dart';
|
||||||
//import '../../../../../services/api_service.dart'; // Import to access DatabaseHelper
|
|
||||||
import 'package:environment_monitoring_app/services/database_helper.dart';
|
import 'package:environment_monitoring_app/services/database_helper.dart';
|
||||||
import '../../../../../services/river_in_situ_sampling_service.dart';
|
import '../../../../../services/river_in_situ_sampling_service.dart';
|
||||||
import '../../../../../bluetooth/bluetooth_manager.dart';
|
import '../../../../../bluetooth/bluetooth_manager.dart';
|
||||||
@ -37,17 +36,12 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
bool _isAutoReading = false;
|
bool _isAutoReading = false;
|
||||||
StreamSubscription? _dataSubscription;
|
StreamSubscription? _dataSubscription;
|
||||||
|
|
||||||
// --- START: Added for lockout timer ---
|
|
||||||
Timer? _lockoutTimer;
|
Timer? _lockoutTimer;
|
||||||
int _lockoutSecondsRemaining = 30;
|
int _lockoutSecondsRemaining = 30;
|
||||||
bool _isLockedOut = false;
|
bool _isLockedOut = false;
|
||||||
// --- END: Added for lockout timer ---
|
|
||||||
|
|
||||||
late final RiverInSituSamplingService _samplingService;
|
late final RiverInSituSamplingService _samplingService;
|
||||||
|
|
||||||
// --- START: Added for direct database access ---
|
|
||||||
final DatabaseHelper _dbHelper = DatabaseHelper();
|
final DatabaseHelper _dbHelper = DatabaseHelper();
|
||||||
// --- END: Added for direct database access ---
|
|
||||||
|
|
||||||
Map<String, double>? _previousReadingsForComparison;
|
Map<String, double>? _previousReadingsForComparison;
|
||||||
Set<String> _outOfBoundsKeys = {};
|
Set<String> _outOfBoundsKeys = {};
|
||||||
@ -67,7 +61,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
|
|
||||||
final List<Map<String, dynamic>> _parameters = [];
|
final List<Map<String, dynamic>> _parameters = [];
|
||||||
|
|
||||||
// Sonde parameter controllers
|
|
||||||
final _sondeIdController = TextEditingController();
|
final _sondeIdController = TextEditingController();
|
||||||
final _dateController = TextEditingController();
|
final _dateController = TextEditingController();
|
||||||
final _timeController = TextEditingController();
|
final _timeController = TextEditingController();
|
||||||
@ -82,13 +75,14 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
final _ammoniaController = TextEditingController();
|
final _ammoniaController = TextEditingController();
|
||||||
final _batteryController = TextEditingController();
|
final _batteryController = TextEditingController();
|
||||||
|
|
||||||
// Flowrate controllers and state
|
|
||||||
String? _selectedFlowrateMethod;
|
String? _selectedFlowrateMethod;
|
||||||
final _flowrateValueController = TextEditingController();
|
final _flowrateValueController = TextEditingController();
|
||||||
final _sdHeightController = TextEditingController();
|
final _sdHeightController = TextEditingController();
|
||||||
final _sdDistanceController = TextEditingController();
|
final _sdDistanceController = TextEditingController();
|
||||||
final _sdTimeFirstController = TextEditingController();
|
|
||||||
final _sdTimeLastController = TextEditingController();
|
final _sdDurationHourController = TextEditingController();
|
||||||
|
final _sdDurationMinuteController = TextEditingController();
|
||||||
|
final _sdDurationSecondController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -102,14 +96,11 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_dataSubscription?.cancel();
|
_dataSubscription?.cancel();
|
||||||
_lockoutTimer?.cancel(); // --- MODIFICATION: Cancel timer on dispose ---
|
_lockoutTimer?.cancel();
|
||||||
|
|
||||||
if (_samplingService.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
// --- MODIFICATION: Ensure clean disconnect on dispose without setState ---
|
||||||
_samplingService.disconnectFromBluetooth();
|
_disconnectFromAll(isDisposing: true);
|
||||||
}
|
// --- END MODIFICATION ---
|
||||||
if (_samplingService.serialConnectionState.value != SerialConnectionState.disconnected) {
|
|
||||||
_samplingService.disconnectFromSerial();
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposeControllers();
|
_disposeControllers();
|
||||||
_disposeFlowrateControllers();
|
_disposeFlowrateControllers();
|
||||||
@ -120,26 +111,35 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
if (mounted) {
|
// FIX: Immediate mounted check to prevent defunct exception
|
||||||
// --- START MODIFICATION ---
|
if (!mounted) return;
|
||||||
final service = _samplingService;
|
|
||||||
final btConnecting = service.bluetoothConnectionState.value == BluetoothConnectionState.connecting;
|
|
||||||
final serialConnecting = service.serialConnectionState.value == SerialConnectionState.connecting;
|
|
||||||
|
|
||||||
// If the widget's local state is loading OR the service's state is stuck connecting
|
final service = _samplingService;
|
||||||
if (_isLoading || btConnecting || serialConnecting) {
|
final btConnecting = service.bluetoothConnectionState.value == BluetoothConnectionState.connecting;
|
||||||
// Force-call disconnect to reset both the service's state
|
final serialConnecting = service.serialConnectionState.value == SerialConnectionState.connecting;
|
||||||
// and the local _isLoading flag (inside _disconnect).
|
|
||||||
_disconnectFromAll();
|
if (_isLoading || btConnecting || serialConnecting) {
|
||||||
} else {
|
_disconnectFromAll();
|
||||||
// If not stuck, just a normal refresh
|
} else {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
|
||||||
// --- END MODIFICATION ---
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _clearDataFields() {
|
||||||
|
const defaultValue = '-999.0';
|
||||||
|
_oxyConcController.text = defaultValue;
|
||||||
|
_oxySatController.text = defaultValue;
|
||||||
|
_phController.text = defaultValue;
|
||||||
|
_salinityController.text = defaultValue;
|
||||||
|
_ecController.text = defaultValue;
|
||||||
|
_tempController.text = defaultValue;
|
||||||
|
_tdsController.text = defaultValue;
|
||||||
|
_turbidityController.text = defaultValue;
|
||||||
|
_ammoniaController.text = defaultValue;
|
||||||
|
_batteryController.text = defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
void _initializeControllers() {
|
void _initializeControllers() {
|
||||||
widget.data.dataCaptureDate = widget.data.samplingDate;
|
widget.data.dataCaptureDate = widget.data.samplingDate;
|
||||||
widget.data.dataCaptureTime = widget.data.samplingTime;
|
widget.data.dataCaptureTime = widget.data.samplingTime;
|
||||||
@ -196,75 +196,72 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
_flowrateValueController.text = widget.data.flowrateValue?.toString() ?? '';
|
_flowrateValueController.text = widget.data.flowrateValue?.toString() ?? '';
|
||||||
_sdHeightController.text = widget.data.flowrateSurfaceDrifterHeight?.toString() ?? '';
|
_sdHeightController.text = widget.data.flowrateSurfaceDrifterHeight?.toString() ?? '';
|
||||||
_sdDistanceController.text = widget.data.flowrateSurfaceDrifterDistance?.toString() ?? '';
|
_sdDistanceController.text = widget.data.flowrateSurfaceDrifterDistance?.toString() ?? '';
|
||||||
_sdTimeFirstController.text = widget.data.flowrateSurfaceDrifterTimeFirst ?? '';
|
|
||||||
_sdTimeLastController.text = widget.data.flowrateSurfaceDrifterTimeLast ?? '';
|
if (widget.data.flowrateSurfaceDrifterTimeLast != null &&
|
||||||
|
widget.data.flowrateSurfaceDrifterTimeLast!.contains(':')) {
|
||||||
|
final parts = widget.data.flowrateSurfaceDrifterTimeLast!.split(':');
|
||||||
|
if (parts.length == 3) {
|
||||||
|
_sdDurationHourController.text = parts[0];
|
||||||
|
_sdDurationMinuteController.text = parts[1];
|
||||||
|
_sdDurationSecondController.text = parts[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _disposeFlowrateControllers() {
|
void _disposeFlowrateControllers() {
|
||||||
_flowrateValueController.dispose();
|
_flowrateValueController.dispose();
|
||||||
_sdHeightController.dispose();
|
_sdHeightController.dispose();
|
||||||
_sdDistanceController.dispose();
|
_sdDistanceController.dispose();
|
||||||
_sdTimeFirstController.dispose();
|
_sdDurationHourController.dispose();
|
||||||
_sdTimeLastController.dispose();
|
_sdDurationMinuteController.dispose();
|
||||||
|
_sdDurationSecondController.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onFlowrateMethodChanged(String? value) {
|
void _onFlowrateMethodChanged(String? value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedFlowrateMethod = value;
|
_selectedFlowrateMethod = value;
|
||||||
widget.data.flowrateMethod = value; // Update model immediately
|
widget.data.flowrateMethod = value;
|
||||||
if (value == 'NA') {
|
if (value == 'NA') {
|
||||||
_flowrateValueController.text = 'NA';
|
_flowrateValueController.text = 'NA';
|
||||||
} else if (value == 'Flowmeter') {
|
} else if (value == 'Flowmeter') {
|
||||||
// --- MODIFICATION: Clear flowrate value for Flowmeter ---
|
|
||||||
_flowrateValueController.clear();
|
_flowrateValueController.clear();
|
||||||
// --- END MODIFICATION ---
|
|
||||||
_sdHeightController.clear();
|
_sdHeightController.clear();
|
||||||
_sdDistanceController.clear();
|
_sdDistanceController.clear();
|
||||||
_sdTimeFirstController.clear();
|
_sdDurationHourController.clear();
|
||||||
_sdTimeLastController.clear();
|
_sdDurationMinuteController.clear();
|
||||||
|
_sdDurationSecondController.clear();
|
||||||
|
|
||||||
} else { // Surface Drifter
|
} else { // Surface Drifter
|
||||||
_flowrateValueController.clear(); // Will be calculated
|
_flowrateValueController.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _calculateFlowrate() {
|
void _calculateFlowrate() {
|
||||||
final distance = double.tryParse(_sdDistanceController.text);
|
final distance = double.tryParse(_sdDistanceController.text);
|
||||||
final timeFirstStr = _sdTimeFirstController.text;
|
final hours = int.tryParse(_sdDurationHourController.text) ?? 0;
|
||||||
final timeLastStr = _sdTimeLastController.text;
|
final minutes = int.tryParse(_sdDurationMinuteController.text) ?? 0;
|
||||||
|
final seconds = int.tryParse(_sdDurationSecondController.text) ?? 0;
|
||||||
|
|
||||||
if (distance == null || timeFirstStr.isEmpty || timeLastStr.isEmpty) {
|
if (distance == null) {
|
||||||
_showSnackBar("Please fill in Distance, Time First, and Time Last.", isError: true);
|
_showSnackBar("Please enter the Distance.", isError: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final totalSeconds = (hours * 3600) + (minutes * 60) + seconds;
|
||||||
|
|
||||||
|
if (totalSeconds <= 0) {
|
||||||
|
_showSnackBar("Total duration must be greater than zero.", isError: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final timeFormat = DateFormat("HH:mm:ss");
|
final flowrate = distance / totalSeconds;
|
||||||
final timeFirst = timeFormat.parse(timeFirstStr);
|
|
||||||
final timeLast = timeFormat.parse(timeLastStr);
|
|
||||||
// Use a common date (like today) to allow time difference calculation across midnight
|
|
||||||
final now = DateTime.now();
|
|
||||||
final dateTimeFirst = DateTime(now.year, now.month, now.day, timeFirst.hour, timeFirst.minute, timeFirst.second);
|
|
||||||
var dateTimeLast = DateTime(now.year, now.month, now.day, timeLast.hour, timeLast.minute, timeLast.second);
|
|
||||||
|
|
||||||
// Handle crossing midnight
|
|
||||||
if (dateTimeLast.isBefore(dateTimeFirst)) {
|
|
||||||
dateTimeLast = dateTimeLast.add(const Duration(days: 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
final differenceInSeconds = dateTimeLast.difference(dateTimeFirst).inSeconds;
|
|
||||||
|
|
||||||
if (differenceInSeconds <= 0) {
|
|
||||||
_showSnackBar("Time Last Deploy must be after Time First Deploy.", isError: true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final flowrate = distance / differenceInSeconds;
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_flowrateValueController.text = flowrate.toStringAsFixed(4);
|
_flowrateValueController.text = flowrate.toStringAsFixed(4);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_showSnackBar("Invalid time format. Please use HH:mm:ss.", isError: true);
|
_showSnackBar("Error calculating flowrate.", isError: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,18 +280,21 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleConnectionAttempt(String type) async {
|
Future<void> _handleConnectionAttempt(String type) async {
|
||||||
// Uses the correct _samplingService instance
|
|
||||||
final bool hasPermissions = await _samplingService.requestDevicePermissions();
|
final bool hasPermissions = await _samplingService.requestDevicePermissions();
|
||||||
if (!hasPermissions && mounted) {
|
if (!hasPermissions && mounted) {
|
||||||
_showSnackBar("Bluetooth & Location permissions are required to connect.", isError: true);
|
_showSnackBar("Bluetooth & Location permissions are required to connect.", isError: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_disconnectFromAll();
|
_disconnectFromAll();
|
||||||
await Future.delayed(const Duration(milliseconds: 250)); // Short delay after disconnect
|
_clearDataFields();
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
final bool connectionSuccess = await _connectToDevice(type);
|
final bool connectionSuccess = await _connectToDevice(type);
|
||||||
|
|
||||||
if (connectionSuccess && mounted) {
|
if (connectionSuccess && mounted) {
|
||||||
_dataSubscription?.cancel(); // Cancel previous subscription if any
|
_dataSubscription?.cancel();
|
||||||
final stream = type == 'bluetooth' ? _samplingService.bluetoothDataStream : _samplingService.serialDataStream;
|
final stream = type == 'bluetooth' ? _samplingService.bluetoothDataStream : _samplingService.serialDataStream;
|
||||||
_dataSubscription = stream.listen((readings) {
|
_dataSubscription = stream.listen((readings) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -303,22 +303,21 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
}, onError: (error) {
|
}, onError: (error) {
|
||||||
debugPrint("Error on data stream: $error");
|
debugPrint("Error on data stream: $error");
|
||||||
if (mounted) _showSnackBar("Data stream error: $error", isError: true);
|
if (mounted) _showSnackBar("Data stream error: $error", isError: true);
|
||||||
_disconnect(type); // Disconnect on stream error
|
_disconnect(type);
|
||||||
}, onDone: () {
|
}, onDone: () {
|
||||||
debugPrint("Data stream done.");
|
debugPrint("Data stream done.");
|
||||||
if (mounted) _disconnect(type); // Disconnect when stream closes
|
if (mounted) _disconnect(type);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _connectToDevice(String type) async {
|
Future<bool> _connectToDevice(String type) async {
|
||||||
// Uses the correct _samplingService instance
|
if (mounted) setState(() => _isLoading = true);
|
||||||
setState(() => _isLoading = true);
|
|
||||||
bool success = false;
|
bool success = false;
|
||||||
try {
|
try {
|
||||||
if (type == 'bluetooth') {
|
if (type == 'bluetooth') {
|
||||||
final devices = await _samplingService.getPairedBluetoothDevices();
|
final devices = await _samplingService.getPairedBluetoothDevices();
|
||||||
if (!mounted) return false; // Check mounted after async gap
|
if (!mounted) return false;
|
||||||
if (devices.isEmpty) {
|
if (devices.isEmpty) {
|
||||||
_showSnackBar('No paired Bluetooth devices found.', isError: true);
|
_showSnackBar('No paired Bluetooth devices found.', isError: true);
|
||||||
return false;
|
return false;
|
||||||
@ -350,13 +349,14 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START MODIFICATION: Countdown Timer Logic ---
|
|
||||||
void _startLockoutTimer() {
|
void _startLockoutTimer() {
|
||||||
_lockoutTimer?.cancel();
|
_lockoutTimer?.cancel();
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_isLockedOut = true;
|
setState(() {
|
||||||
_lockoutSecondsRemaining = 30;
|
_isLockedOut = true;
|
||||||
});
|
_lockoutSecondsRemaining = 30;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_lockoutTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
_lockoutTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
if (_lockoutSecondsRemaining > 0) {
|
if (_lockoutSecondsRemaining > 0) {
|
||||||
@ -375,26 +375,25 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// --- END MODIFICATION ---
|
|
||||||
|
|
||||||
void _toggleAutoReading(String activeType) {
|
void _toggleAutoReading(String activeType) {
|
||||||
final service = context.read<RiverInSituSamplingService>();
|
final service = context.read<RiverInSituSamplingService>();
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_isAutoReading = !_isAutoReading;
|
setState(() {
|
||||||
if (_isAutoReading) {
|
_isAutoReading = !_isAutoReading;
|
||||||
if (activeType == 'bluetooth') service.startBluetoothAutoReading(); else service.startSerialAutoReading();
|
if (_isAutoReading) {
|
||||||
_startLockoutTimer(); // --- MODIFICATION: Start countdown
|
if (activeType == 'bluetooth') service.startBluetoothAutoReading(); else service.startSerialAutoReading();
|
||||||
} else {
|
_startLockoutTimer();
|
||||||
if (activeType == 'bluetooth') service.stopBluetoothAutoReading(); else service.stopSerialAutoReading();
|
} else {
|
||||||
// NOTE: _lockoutTimer is intentionally NOT cancelled here so the lockout persists for the remaining duration
|
if (activeType == 'bluetooth') service.stopBluetoothAutoReading(); else service.stopSerialAutoReading();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _disconnect(String type) {
|
// MODIFIED: Added isDisposing flag to guard setState
|
||||||
// --- START MODIFICATION ---
|
void _disconnect(String type, {bool isDisposing = false}) {
|
||||||
final service = _samplingService; // NEW: Use the member variable
|
final service = _samplingService;
|
||||||
// --- END MODIFICATION ---
|
|
||||||
if (type == 'bluetooth') {
|
if (type == 'bluetooth') {
|
||||||
service.disconnectFromBluetooth();
|
service.disconnectFromBluetooth();
|
||||||
} else {
|
} else {
|
||||||
@ -402,63 +401,59 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
}
|
}
|
||||||
_dataSubscription?.cancel();
|
_dataSubscription?.cancel();
|
||||||
_dataSubscription = null;
|
_dataSubscription = null;
|
||||||
_lockoutTimer?.cancel(); // --- MODIFICATION: Cancel timer on disconnect ---
|
_lockoutTimer?.cancel();
|
||||||
if (mounted) {
|
|
||||||
|
_clearDataFields();
|
||||||
|
|
||||||
|
// FIX: Only call setState if widget is still mounted AND we are not disposing
|
||||||
|
if (mounted && !isDisposing) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isAutoReading = false;
|
_isAutoReading = false;
|
||||||
_isLockedOut = false; // --- MODIFICATION: Reset lockout state ---
|
_isLockedOut = false;
|
||||||
_isLoading = false; // --- NEW: Also reset the loading flag ---
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _disconnectFromAll() {
|
// MODIFIED: Added isDisposing flag to pass down to _disconnect
|
||||||
// --- START MODIFICATION ---
|
void _disconnectFromAll({bool isDisposing = false}) {
|
||||||
final service = _samplingService; // NEW: Use the member variable
|
final service = _samplingService;
|
||||||
// --- END MODIFICATION ---
|
|
||||||
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||||
_disconnect('bluetooth');
|
_disconnect('bluetooth', isDisposing: isDisposing);
|
||||||
}
|
}
|
||||||
if (service.serialConnectionState.value != SerialConnectionState.disconnected) {
|
if (service.serialConnectionState.value != SerialConnectionState.disconnected) {
|
||||||
_disconnect('serial');
|
_disconnect('serial', isDisposing: isDisposing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateTextFields(Map<String, double> readings) {
|
void _updateTextFields(Map<String, double> readings) {
|
||||||
const defaultValue = -999.0;
|
const defaultValue = -999.0;
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_oxyConcController.text = (readings['Optical Dissolved Oxygen: Compensated mg/L'] ?? defaultValue).toStringAsFixed(5);
|
setState(() {
|
||||||
_oxySatController.text = (readings['Optical Dissolved Oxygen: Compensated % Saturation'] ?? defaultValue).toStringAsFixed(5);
|
_oxyConcController.text = (readings['Optical Dissolved Oxygen: Compensated mg/L'] ?? defaultValue).toStringAsFixed(5);
|
||||||
_phController.text = (readings['PH: PH units'] ?? defaultValue).toStringAsFixed(5);
|
_oxySatController.text = (readings['Optical Dissolved Oxygen: Compensated % Saturation'] ?? defaultValue).toStringAsFixed(5);
|
||||||
_tempController.text = (readings['External Temp: Degrees Celcius'] ?? defaultValue).toStringAsFixed(5);
|
_phController.text = (readings['PH: PH units'] ?? defaultValue).toStringAsFixed(5);
|
||||||
_ecController.text = (readings['Conductivity: us/cm'] ?? defaultValue).toStringAsFixed(5);
|
_tempController.text = (readings['External Temp: Degrees Celcius'] ?? defaultValue).toStringAsFixed(5);
|
||||||
_salinityController.text = (readings['Conductivity: Salinity'] ?? defaultValue).toStringAsFixed(5);
|
_ecController.text = (readings['Conductivity: us/cm'] ?? defaultValue).toStringAsFixed(5);
|
||||||
_tdsController.text = (readings['Conductivity:TDS mg/L'] ?? defaultValue).toStringAsFixed(5);
|
_salinityController.text = (readings['Conductivity: Salinity'] ?? defaultValue).toStringAsFixed(5);
|
||||||
_turbidityController.text = (readings['Turbidity: FNU'] ?? defaultValue).toStringAsFixed(5);
|
_tdsController.text = (readings['Conductivity:TDS mg/L'] ?? defaultValue).toStringAsFixed(5);
|
||||||
_batteryController.text = (readings['Sonde: Battery Voltage'] ?? defaultValue).toStringAsFixed(5);
|
_turbidityController.text = (readings['Turbidity: FNU'] ?? defaultValue).toStringAsFixed(5);
|
||||||
_ammoniaController.text = (readings['Ammonium (NH4+) mg/L'] ?? defaultValue).toStringAsFixed(5);
|
_batteryController.text = (readings['Sonde: Battery Voltage'] ?? defaultValue).toStringAsFixed(5);
|
||||||
});
|
_ammoniaController.text = (readings['Ammonium (NH4+) mg/L'] ?? defaultValue).toStringAsFixed(5);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: MODIFIED VALIDATION FLOW ---
|
|
||||||
void _validateAndProceed() async {
|
void _validateAndProceed() async {
|
||||||
// --- START MODIFICATION: Add lockout check ---
|
|
||||||
if (_isLockedOut) {
|
if (_isLockedOut) {
|
||||||
_showSnackBar("Please wait for the initial reading period to complete.", isError: true);
|
_showSnackBar("Please wait for the initial reading period to complete.", isError: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// --- END MODIFICATION ---
|
|
||||||
|
|
||||||
// --- START MODIFICATION: Disable Next if Connected and Auto Reading is active ---
|
|
||||||
// Check if reading is active or if device is connected but not reading (user must disconnect or stop reading first)
|
|
||||||
// Wait, request was: "either user click stop reading or disconnect button then the next button and flowrate can be used again"
|
|
||||||
// So if _isAutoReading is true, block. If device connected but _isAutoReading is false, ALLOW.
|
|
||||||
|
|
||||||
if (_isAutoReading) {
|
if (_isAutoReading) {
|
||||||
_showStopReadingDialog(); // Still show dialog if reading is actively running
|
_showStopReadingDialog();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Remove the forced disconnect check here because user can proceed if they stopped reading manually
|
|
||||||
|
|
||||||
if (!_formKey.currentState!.validate()) {
|
if (!_formKey.currentState!.validate()) {
|
||||||
return;
|
return;
|
||||||
@ -466,15 +461,14 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
_formKey.currentState!.save();
|
_formKey.currentState!.save();
|
||||||
|
|
||||||
final currentReadings = _captureReadingsToMap();
|
final currentReadings = _captureReadingsToMap();
|
||||||
|
|
||||||
// Directly load river-specific limits from the new table via DatabaseHelper.
|
|
||||||
final List<Map<String, dynamic>> riverLimits = await _dbHelper.loadRiverParameterLimits() ?? [];
|
final List<Map<String, dynamic>> riverLimits = await _dbHelper.loadRiverParameterLimits() ?? [];
|
||||||
|
|
||||||
final outOfBoundsParams = _validateParameters(currentReadings, riverLimits);
|
final outOfBoundsParams = _validateParameters(currentReadings, riverLimits);
|
||||||
|
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_outOfBoundsKeys = outOfBoundsParams.map((p) => _parameters.firstWhere((param) => param['label'] == p['label'])['key'] as String).toSet();
|
setState(() {
|
||||||
});
|
_outOfBoundsKeys = outOfBoundsParams.map((p) => _parameters.firstWhere((param) => param['label'] == p['label'])['key'] as String).toSet();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (outOfBoundsParams.isNotEmpty) {
|
if (outOfBoundsParams.isNotEmpty) {
|
||||||
_showParameterLimitDialog(outOfBoundsParams, currentReadings);
|
_showParameterLimitDialog(outOfBoundsParams, currentReadings);
|
||||||
@ -482,7 +476,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
_saveDataAndMoveOn(currentReadings);
|
_saveDataAndMoveOn(currentReadings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- END: MODIFIED VALIDATION FLOW ---
|
|
||||||
|
|
||||||
Map<String, double> _captureReadingsToMap() {
|
Map<String, double> _captureReadingsToMap() {
|
||||||
final Map<String, double> readings = {};
|
final Map<String, double> readings = {};
|
||||||
@ -551,8 +544,16 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
if (_selectedFlowrateMethod == 'Surface Drifter') {
|
if (_selectedFlowrateMethod == 'Surface Drifter') {
|
||||||
widget.data.flowrateSurfaceDrifterHeight = double.tryParse(_sdHeightController.text);
|
widget.data.flowrateSurfaceDrifterHeight = double.tryParse(_sdHeightController.text);
|
||||||
widget.data.flowrateSurfaceDrifterDistance = double.tryParse(_sdDistanceController.text);
|
widget.data.flowrateSurfaceDrifterDistance = double.tryParse(_sdDistanceController.text);
|
||||||
widget.data.flowrateSurfaceDrifterTimeFirst = _sdTimeFirstController.text;
|
|
||||||
widget.data.flowrateSurfaceDrifterTimeLast = _sdTimeLastController.text;
|
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
||||||
|
String formattedDuration =
|
||||||
|
"${twoDigits(int.tryParse(_sdDurationHourController.text) ?? 0)}:"
|
||||||
|
"${twoDigits(int.tryParse(_sdDurationMinuteController.text) ?? 0)}:"
|
||||||
|
"${twoDigits(int.tryParse(_sdDurationSecondController.text) ?? 0)}";
|
||||||
|
|
||||||
|
widget.data.flowrateSurfaceDrifterTimeFirst = "00:00:00";
|
||||||
|
widget.data.flowrateSurfaceDrifterTimeLast = formattedDuration;
|
||||||
|
|
||||||
widget.data.flowrateValue = double.tryParse(_flowrateValueController.text);
|
widget.data.flowrateValue = double.tryParse(_flowrateValueController.text);
|
||||||
} else if (_selectedFlowrateMethod == 'Flowmeter') {
|
} else if (_selectedFlowrateMethod == 'Flowmeter') {
|
||||||
widget.data.flowrateSurfaceDrifterHeight = null;
|
widget.data.flowrateSurfaceDrifterHeight = null;
|
||||||
@ -573,12 +574,14 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_outOfBoundsKeys.clear();
|
setState(() {
|
||||||
if (_previousReadingsForComparison != null) {
|
_outOfBoundsKeys.clear();
|
||||||
_previousReadingsForComparison = null;
|
if (_previousReadingsForComparison != null) {
|
||||||
}
|
_previousReadingsForComparison = null;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
widget.onNext();
|
widget.onNext();
|
||||||
}
|
}
|
||||||
@ -608,9 +611,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic>? _getActiveConnectionDetails() {
|
Map<String, dynamic>? _getActiveConnectionDetails() {
|
||||||
// --- START FIX: Use read() instead of watch() ---
|
|
||||||
final service = context.read<RiverInSituSamplingService>();
|
final service = context.read<RiverInSituSamplingService>();
|
||||||
// --- END FIX ---
|
|
||||||
|
|
||||||
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
if (service.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||||
return {'type': 'bluetooth', 'state': service.bluetoothConnectionState.value, 'name': service.connectedBluetoothDeviceName};
|
return {'type': 'bluetooth', 'state': service.bluetoothConnectionState.value, 'name': service.connectedBluetoothDeviceName};
|
||||||
@ -627,23 +628,16 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
final activeConnection = _getActiveConnectionDetails();
|
final activeConnection = _getActiveConnectionDetails();
|
||||||
final String? activeType = activeConnection?['type'] as String?;
|
final String? activeType = activeConnection?['type'] as String?;
|
||||||
|
|
||||||
// Check if ANY device is currently connected
|
|
||||||
final bool isDeviceConnected = activeConnection != null;
|
|
||||||
|
|
||||||
// --- START MODIFICATION: Logic for disabling inputs ---
|
|
||||||
// Disable interaction if auto-reading is active OR if locked out.
|
|
||||||
// If reading is stopped (even if connected), we allow interaction.
|
|
||||||
final bool shouldDisableInput = _isAutoReading || _isLockedOut;
|
final bool shouldDisableInput = _isAutoReading || _isLockedOut;
|
||||||
// --- END MODIFICATION ---
|
|
||||||
|
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
if (_isLockedOut) {
|
if (_isLockedOut) {
|
||||||
_showSnackBar("Please wait for the initial reading period to complete.", isError: true);
|
_showSnackBar("Please wait for the initial reading period to complete.", isError: true);
|
||||||
return false; // Prevent back navigation
|
return false;
|
||||||
}
|
}
|
||||||
_disconnectFromAll();
|
_disconnectFromAll();
|
||||||
return true; // Allow back navigation
|
return true;
|
||||||
},
|
},
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@ -674,7 +668,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
ValueListenableBuilder<String?>(
|
ValueListenableBuilder<String?>(
|
||||||
valueListenable: service.sondeId,
|
valueListenable: service.sondeId,
|
||||||
builder: (context, sondeId, child) {
|
builder: (context, sondeId, child) {
|
||||||
// --- START FIX: Only update if non-null to prevent clearing on disconnect ---
|
|
||||||
if (sondeId != null && sondeId.isNotEmpty) {
|
if (sondeId != null && sondeId.isNotEmpty) {
|
||||||
final newSondeId = sondeId;
|
final newSondeId = sondeId;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@ -684,7 +677,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// --- END FIX ---
|
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: _sondeIdController,
|
controller: _sondeIdController,
|
||||||
decoration: const InputDecoration(labelText: 'Sonde ID *', hintText: 'Connect device or enter manually'),
|
decoration: const InputDecoration(labelText: 'Sonde ID *', hintText: 'Connect device or enter manually'),
|
||||||
@ -720,13 +712,10 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
),
|
),
|
||||||
const Divider(height: 32),
|
const Divider(height: 32),
|
||||||
|
|
||||||
// --- MODIFIED: Use 'shouldDisableInput' instead of 'isDeviceConnected' ---
|
|
||||||
_buildFlowrateSection(isInputDisabled: shouldDisableInput),
|
_buildFlowrateSection(isInputDisabled: shouldDisableInput),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
// --- MODIFIED: Enable Next button if reading stopped (even if connected) ---
|
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
// Disable if locked out OR reading is active
|
|
||||||
onPressed: (_isLockedOut || _isAutoReading) ? null : _validateAndProceed,
|
onPressed: (_isLockedOut || _isAutoReading) ? null : _validateAndProceed,
|
||||||
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
|
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -735,7 +724,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
: (_isAutoReading ? 'Stop Reading to Proceed' : 'Next')
|
: (_isAutoReading ? 'Stop Reading to Proceed' : 'Next')
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// --- END MODIFICATION ---
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -786,14 +774,12 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
if (isConnecting || _isLoading)
|
if (isConnecting || _isLoading)
|
||||||
const CircularProgressIndicator()
|
const CircularProgressIndicator()
|
||||||
else if (isConnected)
|
else if (isConnected)
|
||||||
// Replaced Row with Wrap to fix horizontal overflow with countdown timer
|
|
||||||
Wrap(
|
Wrap(
|
||||||
alignment: WrapAlignment.spaceEvenly,
|
alignment: WrapAlignment.spaceEvenly,
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
spacing: 8.0, // Horizontal space between buttons
|
spacing: 8.0,
|
||||||
runSpacing: 4.0, // Vertical space if it wraps
|
runSpacing: 4.0,
|
||||||
children: [
|
children: [
|
||||||
// Add countdown to Stop Reading button
|
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: Icon(_isAutoReading ? Icons.stop_circle_outlined : Icons.play_circle_outlined),
|
icon: Icon(_isAutoReading ? Icons.stop_circle_outlined : Icons.play_circle_outlined),
|
||||||
label: Text(_isAutoReading
|
label: Text(_isAutoReading
|
||||||
@ -810,7 +796,9 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.link_off),
|
icon: const Icon(Icons.link_off),
|
||||||
label: const Text('Disconnect'),
|
label: const Text('Disconnect'),
|
||||||
onPressed: () => _disconnect(type),
|
// --- MODIFICATION: Disable button if locked out ---
|
||||||
|
onPressed: _isLockedOut ? null : () => _disconnect(type),
|
||||||
|
// --- END MODIFICATION ---
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -966,9 +954,11 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Resample'),
|
child: const Text('Resample'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_previousReadingsForComparison = readings;
|
setState(() {
|
||||||
});
|
_previousReadingsForComparison = readings;
|
||||||
|
});
|
||||||
|
}
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -985,7 +975,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated to include disable logic
|
|
||||||
Widget _buildFlowrateSection({bool isInputDisabled = false}) {
|
Widget _buildFlowrateSection({bool isInputDisabled = false}) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
@ -1012,14 +1001,12 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// Wrap content in AbsorbPointer and Opacity if connected
|
|
||||||
AbsorbPointer(
|
AbsorbPointer(
|
||||||
absorbing: isInputDisabled,
|
absorbing: isInputDisabled,
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: isInputDisabled ? 0.5 : 1.0,
|
opacity: isInputDisabled ? 0.5 : 1.0,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Replaced Row with Wrap to fix horizontal overflow for radio buttons
|
|
||||||
Wrap(
|
Wrap(
|
||||||
alignment: WrapAlignment.spaceAround,
|
alignment: WrapAlignment.spaceAround,
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
@ -1027,10 +1014,9 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
children: [
|
children: [
|
||||||
_buildFlowrateRadioButton("Surface Drifter"),
|
_buildFlowrateRadioButton("Surface Drifter"),
|
||||||
_buildFlowrateRadioButton("Flowmeter"),
|
_buildFlowrateRadioButton("Flowmeter"),
|
||||||
_buildFlowrateRadioButton("NA"), // Not Applicable
|
_buildFlowrateRadioButton("NA"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// Conditional fields based on selected method
|
|
||||||
if (_selectedFlowrateMethod == 'Surface Drifter')
|
if (_selectedFlowrateMethod == 'Surface Drifter')
|
||||||
_buildSurfaceDrifterFields(),
|
_buildSurfaceDrifterFields(),
|
||||||
if (_selectedFlowrateMethod == 'Flowmeter')
|
if (_selectedFlowrateMethod == 'Flowmeter')
|
||||||
@ -1058,7 +1044,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
overflow: TextOverflow.ellipsis, // Add ellipsis handling for safety
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -1068,12 +1054,12 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _sdHeightController,
|
controller: _sdHeightController,
|
||||||
decoration: const InputDecoration(labelText: 'Height (m)'),
|
decoration: const InputDecoration(labelText: 'Height (m)'),
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
// Add validation if needed
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@ -1083,20 +1069,46 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
validator: (v) => v == null || v.isEmpty ? 'Distance is required' : null,
|
validator: (v) => v == null || v.isEmpty ? 'Distance is required' : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
const Text("Duration of Travel", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
|
||||||
controller: _sdTimeFirstController,
|
const SizedBox(height: 8),
|
||||||
decoration: const InputDecoration(labelText: 'Time First Deploy (HH:mm:ss) *', suffixIcon: Icon(Icons.timer)),
|
Row(
|
||||||
readOnly: true,
|
children: [
|
||||||
onTap: () => _selectTime(context, _sdTimeFirstController),
|
Expanded(
|
||||||
validator: (v) => v == null || v.isEmpty ? 'Start time is required' : null,
|
child: TextFormField(
|
||||||
),
|
controller: _sdDurationHourController,
|
||||||
const SizedBox(height: 16),
|
decoration: const InputDecoration(
|
||||||
TextFormField(
|
labelText: 'Hours',
|
||||||
controller: _sdTimeLastController,
|
counterText: "",
|
||||||
decoration: const InputDecoration(labelText: 'Time Last Deploy (HH:mm:ss) *', suffixIcon: Icon(Icons.timer)),
|
),
|
||||||
readOnly: true,
|
keyboardType: TextInputType.number,
|
||||||
onTap: () => _selectTime(context, _sdTimeLastController),
|
maxLength: 2,
|
||||||
validator: (v) => v == null || v.isEmpty ? 'End time is required' : null,
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _sdDurationMinuteController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Minutes',
|
||||||
|
counterText: "",
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _sdDurationSecondController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Seconds',
|
||||||
|
counterText: "",
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
@ -1108,7 +1120,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
controller: _flowrateValueController,
|
controller: _flowrateValueController,
|
||||||
decoration: const InputDecoration(labelText: 'Calculated Flowrate (m/s)'),
|
decoration: const InputDecoration(labelText: 'Calculated Flowrate (m/s)'),
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
// Add validator if calculation must be done?
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -1128,7 +1139,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNAField() {
|
Widget _buildNAField() {
|
||||||
// Fix: Use controller to set value instead of initialValue to avoid conflict crash
|
|
||||||
if (_flowrateValueController.text != 'NA') {
|
if (_flowrateValueController.text != 'NA') {
|
||||||
_flowrateValueController.text = 'NA';
|
_flowrateValueController.text = 'NA';
|
||||||
}
|
}
|
||||||
@ -1138,8 +1148,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _flowrateValueController,
|
controller: _flowrateValueController,
|
||||||
decoration: const InputDecoration(labelText: 'Flowrate (m/s)'),
|
decoration: const InputDecoration(labelText: 'Flowrate (m/s)'),
|
||||||
// initialValue: 'NA', // Removed to fix AssertionError: initialValue == null || controller == null
|
readOnly: true,
|
||||||
readOnly: true, // Make it read-only
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,6 +69,9 @@ class _RiverInSituStep4AdditionalInfoState
|
|||||||
stationCode: stationCode, // Pass the station code here
|
stationCode: stationCode, // Pass the station code here
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// FIX: Check if widget is still mounted after the await call to prevent defunct exception
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
setState(() => setImageCallback(file));
|
setState(() => setImageCallback(file));
|
||||||
} else if (mounted) {
|
} else if (mounted) {
|
||||||
|
|||||||
@ -221,7 +221,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.12.03'),
|
subtitle: const Text('MMS Version 3.12.06'),
|
||||||
dense: true,
|
dense: true,
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|||||||
@ -179,6 +179,36 @@ class ApiService {
|
|||||||
await _baseService.get(baseUrl, 'profile');
|
await _baseService.get(baseUrl, 'profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- NEW METHOD FOR FIRST TIME LOGIN ---
|
||||||
|
/// Fetches critical configuration data (API and FTP settings) needed for
|
||||||
|
/// immediate operation. This should be called during the login/splash sequence.
|
||||||
|
Future<void> fetchInitialConfigurations() async {
|
||||||
|
debugPrint("ApiService: Fetching initial API and FTP configurations...");
|
||||||
|
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Fetch API Configs
|
||||||
|
final apiResult = await _baseService.get(baseUrl, 'api-configs');
|
||||||
|
if (apiResult['success'] == true && apiResult['data'] != null) {
|
||||||
|
final apiData = List<Map<String, dynamic>>.from(apiResult['data']['updated'] ?? []);
|
||||||
|
await dbHelper.upsertApiConfigs(apiData);
|
||||||
|
debugPrint("Initial API Configs synced: ${apiData.length} items.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fetch FTP Configs
|
||||||
|
final ftpResult = await _baseService.get(baseUrl, 'ftp-configs');
|
||||||
|
if (ftpResult['success'] == true && ftpResult['data'] != null) {
|
||||||
|
final ftpData = List<Map<String, dynamic>>.from(ftpResult['data']['updated'] ?? []);
|
||||||
|
await dbHelper.upsertFtpConfigs(ftpData);
|
||||||
|
debugPrint("Initial FTP Configs synced: ${ftpData.length} items.");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("ApiService: Error fetching initial configurations: $e");
|
||||||
|
// We don't rethrow here to allow login to proceed, but UserPreferencesService
|
||||||
|
// will handle the missing data gracefully (by not saving defaults yet).
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> _fetchDelta(String endpoint, String? lastSyncTimestamp) async {
|
Future<Map<String, dynamic>> _fetchDelta(String endpoint, String? lastSyncTimestamp) async {
|
||||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||||
String url = endpoint;
|
String url = endpoint;
|
||||||
|
|||||||
@ -53,6 +53,10 @@ class MarineInSituSamplingService {
|
|||||||
final TelegramService _telegramService;
|
final TelegramService _telegramService;
|
||||||
final UserPreferencesService _userPreferencesService = UserPreferencesService(); // ADDED
|
final UserPreferencesService _userPreferencesService = UserPreferencesService(); // ADDED
|
||||||
|
|
||||||
|
// --- START FIX: Activity Tracker ---
|
||||||
|
bool _isDisposed = false;
|
||||||
|
// --- END FIX ---
|
||||||
|
|
||||||
MarineInSituSamplingService(this._telegramService);
|
MarineInSituSamplingService(this._telegramService);
|
||||||
|
|
||||||
static const platform = MethodChannel('com.example.environment_monitoring_app/usb');
|
static const platform = MethodChannel('com.example.environment_monitoring_app/usb');
|
||||||
@ -69,7 +73,7 @@ class MarineInSituSamplingService {
|
|||||||
}) async {
|
}) async {
|
||||||
final picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
final XFile? photo = await picker.pickImage(source: source, imageQuality: 85, maxWidth: 1024);
|
final XFile? photo = await picker.pickImage(source: source, imageQuality: 85, maxWidth: 1024);
|
||||||
if (photo == null) return null;
|
if (photo == null || _isDisposed) return null;
|
||||||
|
|
||||||
final bytes = await photo.readAsBytes();
|
final bytes = await photo.readAsBytes();
|
||||||
img.Image? originalImage = img.decodeImage(bytes);
|
img.Image? originalImage = img.decodeImage(bytes);
|
||||||
@ -156,11 +160,18 @@ class MarineInSituSamplingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void disconnectFromSerial() => _serialManager.disconnect();
|
// --- START FIX: Handle Thread Interruption during disconnect ---
|
||||||
|
void disconnectFromSerial() {
|
||||||
|
stopSerialAutoReading();
|
||||||
|
_serialManager.disconnect();
|
||||||
|
}
|
||||||
|
// --- END FIX ---
|
||||||
|
|
||||||
void startSerialAutoReading({Duration? interval}) => _serialManager.startAutoReading(interval: interval ?? const Duration(seconds: 2));
|
void startSerialAutoReading({Duration? interval}) => _serialManager.startAutoReading(interval: interval ?? const Duration(seconds: 2));
|
||||||
void stopSerialAutoReading() => _serialManager.stopAutoReading();
|
void stopSerialAutoReading() => _serialManager.stopAutoReading();
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_isDisposed = true;
|
||||||
_bluetoothManager.dispose();
|
_bluetoothManager.dispose();
|
||||||
_serialManager.dispose();
|
_serialManager.dispose();
|
||||||
}
|
}
|
||||||
@ -333,14 +344,14 @@ class MarineInSituSamplingService {
|
|||||||
bool anyFtpSuccess = false;
|
bool anyFtpSuccess = false;
|
||||||
|
|
||||||
// --- START FIX: Check if FTP is enabled AND if it was already successful ---
|
// --- START FIX: Check if FTP is enabled AND if it was already successful ---
|
||||||
bool previousFtpSuccess = data.submissionStatus == 'L4' || data.submissionStatus == 'S4';
|
bool previousFtpSuccess = previousStatus == 'L4' || previousStatus == 'S4';
|
||||||
|
|
||||||
if (!isFtpEnabled) {
|
if (!isFtpEnabled) {
|
||||||
debugPrint("FTP submission disabled for $moduleName by user preference. Skipping FTP.");
|
debugPrint("FTP submission disabled for $moduleName by user preference. Skipping FTP.");
|
||||||
ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'FTP disabled by user preference.', 'success': true}]};
|
ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'FTP disabled by user preference.', 'success': true}]};
|
||||||
anyFtpSuccess = true;
|
anyFtpSuccess = true;
|
||||||
} else if (previousFtpSuccess) {
|
} else if (previousFtpSuccess) {
|
||||||
debugPrint("FTP submission skipped because it was already successful (Status: ${data.submissionStatus}).");
|
debugPrint("FTP submission skipped because it was already successful (Status: $previousStatus).");
|
||||||
ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'Already successful in previous attempt.', 'success': true}]};
|
ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'Already successful in previous attempt.', 'success': true}]};
|
||||||
anyFtpSuccess = true;
|
anyFtpSuccess = true;
|
||||||
} else {
|
} else {
|
||||||
@ -478,10 +489,7 @@ class MarineInSituSamplingService {
|
|||||||
// Save/Update local log first
|
// Save/Update local log first
|
||||||
if (savedLogPath != null && savedLogPath.isNotEmpty) {
|
if (savedLogPath != null && savedLogPath.isNotEmpty) {
|
||||||
// Need to reconstruct the map with file paths for updating
|
// Need to reconstruct the map with file paths for updating
|
||||||
// --- START: MODIFICATION (FIXED ERROR) ---
|
|
||||||
// Changed data.toDbJson() to data.toMap() to get a Map, not a String.
|
|
||||||
Map<String, dynamic> logUpdateData = data.toMap();
|
Map<String, dynamic> logUpdateData = data.toMap();
|
||||||
// --- END: MODIFICATION (FIXED ERROR) ---
|
|
||||||
final imageFiles = data.toApiImageFiles();
|
final imageFiles = data.toApiImageFiles();
|
||||||
imageFiles.forEach((key, file) {
|
imageFiles.forEach((key, file) {
|
||||||
logUpdateData[key] = file?.path; // Add paths back
|
logUpdateData[key] = file?.path; // Add paths back
|
||||||
@ -515,8 +523,6 @@ class MarineInSituSamplingService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
|
const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
|
||||||
// Log final queued state to central DB
|
|
||||||
// await _logAndSave(data: data, status: 'Queued', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}, apiRecordId: null, logDirectory: savedLogPath);
|
|
||||||
|
|
||||||
return {'success': true, 'message': successMessage, 'reportId': data.reportId}; // Return timestamp ID
|
return {'success': true, 'message': successMessage, 'reportId': data.reportId}; // Return timestamp ID
|
||||||
}
|
}
|
||||||
@ -622,10 +628,7 @@ class MarineInSituSamplingService {
|
|||||||
final baseFileName = _generateBaseFileName(data); // Use helper
|
final baseFileName = _generateBaseFileName(data); // Use helper
|
||||||
|
|
||||||
// Prepare log data map including file paths
|
// Prepare log data map including file paths
|
||||||
// --- START: MODIFICATION (FIXED ERROR) ---
|
|
||||||
// Changed data.toDbJson() to data.toMap() to get a Map, not a String.
|
|
||||||
Map<String, dynamic> logMapData = data.toMap();
|
Map<String, dynamic> logMapData = data.toMap();
|
||||||
// --- END: MODIFICATION (FIXED ERROR) ---
|
|
||||||
final imageFileMap = data.toApiImageFiles();
|
final imageFileMap = data.toApiImageFiles();
|
||||||
imageFileMap.forEach((key, file) {
|
imageFileMap.forEach((key, file) {
|
||||||
logMapData[key] = file?.path; // Store path or null
|
logMapData[key] = file?.path; // Store path or null
|
||||||
@ -795,7 +798,8 @@ class MarineInSituSamplingService {
|
|||||||
|
|
||||||
// Find the limit data for this parameter AND this specific station
|
// Find the limit data for this parameter AND this specific station
|
||||||
final limitData = allLimits.firstWhere(
|
final limitData = allLimits.firstWhere(
|
||||||
(l) => l['param_parameter_list'] == limitName && l['station_id']?.toString() == stationId.toString(),
|
(l) => l['param_parameter_list'] == limitName &&
|
||||||
|
(l['station_id']?.toString() == stationId.toString() || l['man_station_id']?.toString() == stationId.toString()),
|
||||||
orElse: () => <String, dynamic>{}, // Use explicit type
|
orElse: () => <String, dynamic>{}, // Use explicit type
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -888,7 +892,7 @@ class MarineInSituSamplingService {
|
|||||||
if (isHit) {
|
if (isHit) {
|
||||||
final valueStr = value.toStringAsFixed(5);
|
final valueStr = value.toStringAsFixed(5);
|
||||||
final lowerStr = lowerLimit?.toStringAsFixed(5) ?? 'N/A';
|
final lowerStr = lowerLimit?.toStringAsFixed(5) ?? 'N/A';
|
||||||
final upperStr = upperLimit?.toStringAsFixed(5) ?? 'N/á';
|
final upperStr = upperLimit?.toStringAsFixed(5) ?? 'N/A';
|
||||||
String limitStr;
|
String limitStr;
|
||||||
if (lowerStr != 'N/A' && upperStr != 'N/A') {
|
if (lowerStr != 'N/A' && upperStr != 'N/A') {
|
||||||
limitStr = '$lowerStr - $upperStr';
|
limitStr = '$lowerStr - $upperStr';
|
||||||
|
|||||||
@ -52,6 +52,10 @@ class MarineInvestigativeSamplingService {
|
|||||||
final TelegramService _telegramService;
|
final TelegramService _telegramService;
|
||||||
final UserPreferencesService _userPreferencesService = UserPreferencesService(); // ADDED
|
final UserPreferencesService _userPreferencesService = UserPreferencesService(); // ADDED
|
||||||
|
|
||||||
|
// --- START FIX: Activity Tracker ---
|
||||||
|
bool _isDisposed = false;
|
||||||
|
// --- END FIX ---
|
||||||
|
|
||||||
MarineInvestigativeSamplingService(this._telegramService);
|
MarineInvestigativeSamplingService(this._telegramService);
|
||||||
|
|
||||||
static const platform = MethodChannel('com.example.environment_monitoring_app/usb');
|
static const platform = MethodChannel('com.example.environment_monitoring_app/usb');
|
||||||
@ -68,7 +72,9 @@ class MarineInvestigativeSamplingService {
|
|||||||
}) async {
|
}) async {
|
||||||
final picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
final XFile? photo = await picker.pickImage(source: source, imageQuality: 85, maxWidth: 1024);
|
final XFile? photo = await picker.pickImage(source: source, imageQuality: 85, maxWidth: 1024);
|
||||||
if (photo == null) return null;
|
|
||||||
|
// --- FIX: Check if photo is null or service is disposed ---
|
||||||
|
if (photo == null || _isDisposed) return null;
|
||||||
|
|
||||||
final bytes = await photo.readAsBytes();
|
final bytes = await photo.readAsBytes();
|
||||||
img.Image? originalImage = img.decodeImage(bytes);
|
img.Image? originalImage = img.decodeImage(bytes);
|
||||||
@ -164,11 +170,18 @@ class MarineInvestigativeSamplingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void disconnectFromSerial() => _serialManager.disconnect();
|
// --- START FIX: Handle Thread Interruption during disconnect ---
|
||||||
|
void disconnectFromSerial() {
|
||||||
|
stopSerialAutoReading();
|
||||||
|
_serialManager.disconnect();
|
||||||
|
}
|
||||||
|
// --- END FIX ---
|
||||||
|
|
||||||
void startSerialAutoReading({Duration? interval}) => _serialManager.startAutoReading(interval: interval ?? const Duration(seconds: 2));
|
void startSerialAutoReading({Duration? interval}) => _serialManager.startAutoReading(interval: interval ?? const Duration(seconds: 2));
|
||||||
void stopSerialAutoReading() => _serialManager.stopAutoReading();
|
void stopSerialAutoReading() => _serialManager.stopAutoReading();
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_isDisposed = true; // --- FIX: Track disposal ---
|
||||||
_bluetoothManager.dispose();
|
_bluetoothManager.dispose();
|
||||||
_serialManager.dispose();
|
_serialManager.dispose();
|
||||||
}
|
}
|
||||||
@ -459,7 +472,7 @@ class MarineInvestigativeSamplingService {
|
|||||||
Future<Map<String, dynamic>> _performOfflineQueuing({
|
Future<Map<String, dynamic>> _performOfflineQueuing({
|
||||||
required MarineInvesManualSamplingData data,
|
required MarineInvesManualSamplingData data,
|
||||||
required String moduleName,
|
required String moduleName,
|
||||||
String? logDirectory, // Added for potential update
|
String? logDirectory, // Pass for potential update
|
||||||
}) async {
|
}) async {
|
||||||
final serverConfig = await _serverConfigService.getActiveApiConfig();
|
final serverConfig = await _serverConfigService.getActiveApiConfig();
|
||||||
final serverName = serverConfig?['config_name'] as String? ?? 'Default';
|
final serverName = serverConfig?['config_name'] as String? ?? 'Default';
|
||||||
@ -789,8 +802,11 @@ class MarineInvestigativeSamplingService {
|
|||||||
final limitName = _parameterKeyToLimitName[key];
|
final limitName = _parameterKeyToLimitName[key];
|
||||||
if (limitName == null) return;
|
if (limitName == null) return;
|
||||||
|
|
||||||
|
// --- FIX: Ensure robust string-based ID comparison ---
|
||||||
final limitData = allLimits.firstWhere(
|
final limitData = allLimits.firstWhere(
|
||||||
(l) => l['param_parameter_list'] == limitName && l['station_id']?.toString() == stationId.toString(),
|
(l) => l['param_parameter_list'] == limitName &&
|
||||||
|
(l['station_id']?.toString() == stationId.toString() ||
|
||||||
|
l['man_station_id']?.toString() == stationId.toString()),
|
||||||
orElse: () => <String, dynamic>{},
|
orElse: () => <String, dynamic>{},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -902,5 +918,4 @@ class MarineInvestigativeSamplingService {
|
|||||||
|
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
// --- END: NEW METHOD ---
|
|
||||||
}
|
}
|
||||||
@ -35,17 +35,29 @@ class UserPreferencesService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint("Applying and auto-saving default submission preferences for the first time.");
|
debugPrint("Checking availability of configs for default preference application...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all possible configs from the database just once
|
// Get all possible configs from the database just once
|
||||||
final allApiConfigs = await _dbHelper.loadApiConfigs() ?? [];
|
final allApiConfigs = await _dbHelper.loadApiConfigs() ?? [];
|
||||||
final allFtpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
final allFtpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
||||||
|
|
||||||
|
// --- CRITICAL CHECK ---
|
||||||
|
// If we haven't synced data from the server yet, do NOT attempt to apply defaults.
|
||||||
|
// If we proceed now, we would save "empty" preferences and the flag would be set to true,
|
||||||
|
// preventing the defaults from ever being applied correctly later.
|
||||||
|
if (allApiConfigs.isEmpty && allFtpConfigs.isEmpty) {
|
||||||
|
debugPrint("No API or FTP configs found in local DB. Skipping default application. Will retry later.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint("Configs found. Applying and auto-saving default submission preferences.");
|
||||||
|
|
||||||
for (var module in _configurableModules) {
|
for (var module in _configurableModules) {
|
||||||
final moduleKey = module['key']!;
|
final moduleKey = module['key']!;
|
||||||
|
|
||||||
// 1. Save master switches to enable API and FTP for the module.
|
// 1. Save master switches to enable API and FTP for the module.
|
||||||
|
// FORCE them to be TRUE by default.
|
||||||
await saveModulePreference(
|
await saveModulePreference(
|
||||||
moduleName: moduleKey,
|
moduleName: moduleKey,
|
||||||
isApiEnabled: true,
|
isApiEnabled: true,
|
||||||
@ -54,8 +66,10 @@ class UserPreferencesService {
|
|||||||
|
|
||||||
// 2. Determine default API links
|
// 2. Determine default API links
|
||||||
final defaultApiLinks = allApiConfigs.map((config) {
|
final defaultApiLinks = allApiConfigs.map((config) {
|
||||||
bool isActive = (config['is_active'] == 1 || config['is_active'] == true);
|
// Robust check for active status (handles int 1, string '1', bool true)
|
||||||
bool isPstwHq = (config['config_name'] == 'PSTW_HQ');
|
final activeVal = config['is_active'];
|
||||||
|
final bool isActive = activeVal == 1 || activeVal == true || activeVal.toString() == '1';
|
||||||
|
final bool isPstwHq = (config['config_name'] == 'PSTW_HQ');
|
||||||
|
|
||||||
bool isEnabled;
|
bool isEnabled;
|
||||||
|
|
||||||
@ -82,7 +96,10 @@ class UserPreferencesService {
|
|||||||
|
|
||||||
final defaultFtpLinks = allFtpConfigs.map((config) {
|
final defaultFtpLinks = allFtpConfigs.map((config) {
|
||||||
final String configModule = config['ftp_module'] ?? '';
|
final String configModule = config['ftp_module'] ?? '';
|
||||||
final bool isActive = (config['is_active'] == 1 || config['is_active'] == true);
|
|
||||||
|
// Robust check for active status (Handles your SQL data where is_active is 1)
|
||||||
|
final activeVal = config['is_active'];
|
||||||
|
final bool isActive = activeVal == 1 || activeVal == true || activeVal.toString() == '1';
|
||||||
|
|
||||||
// Enable if the config's module matches the current moduleKey AND it's active
|
// Enable if the config's module matches the current moduleKey AND it's active
|
||||||
bool isEnabled = (configModule == expectedFtpModuleKey) && isActive;
|
bool isEnabled = (configModule == expectedFtpModuleKey) && isActive;
|
||||||
@ -107,13 +124,14 @@ class UserPreferencesService {
|
|||||||
|
|
||||||
|
|
||||||
/// Retrieves a module's master submission preferences.
|
/// Retrieves a module's master submission preferences.
|
||||||
/// This method now returns null if no preference is found.
|
/// This method now returns a default TRUE object if no preference is found.
|
||||||
Future<Map<String, dynamic>?> getModulePreference(String moduleName) async {
|
Future<Map<String, dynamic>?> getModulePreference(String moduleName) async {
|
||||||
final preference = await _dbHelper.getModulePreference(moduleName);
|
final preference = await _dbHelper.getModulePreference(moduleName);
|
||||||
if (preference != null) {
|
if (preference != null) {
|
||||||
return preference;
|
return preference;
|
||||||
}
|
}
|
||||||
return null;
|
// Return default enabled state (TRUE) if not found in DB yet
|
||||||
|
return {'is_api_enabled': true, 'is_ftp_enabled': true};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Saves or updates a module's master on/off switches for API and FTP submissions.
|
/// Saves or updates a module's master on/off switches for API and FTP submissions.
|
||||||
@ -162,8 +180,10 @@ class UserPreferencesService {
|
|||||||
isEnabled = matchingLink['is_enabled'] as bool? ?? false;
|
isEnabled = matchingLink['is_enabled'] as bool? ?? false;
|
||||||
} else {
|
} else {
|
||||||
// No preference saved for this config. Apply default logic.
|
// No preference saved for this config. Apply default logic.
|
||||||
bool isActive = (config['is_active'] == 1 || config['is_active'] == true);
|
// Robust check for active status
|
||||||
bool isPstwHq = (config['config_name'] == 'PSTW_HQ');
|
final activeVal = config['is_active'];
|
||||||
|
final bool isActive = activeVal == 1 || activeVal == true || activeVal.toString() == '1';
|
||||||
|
final bool isPstwHq = (config['config_name'] == 'PSTW_HQ');
|
||||||
|
|
||||||
// --- MODIFIED: Special logic for Marine Report ---
|
// --- MODIFIED: Special logic for Marine Report ---
|
||||||
if (moduleName == 'marine_report') {
|
if (moduleName == 'marine_report') {
|
||||||
@ -218,7 +238,10 @@ class UserPreferencesService {
|
|||||||
} else {
|
} else {
|
||||||
// No preference saved for this config. Apply default logic.
|
// No preference saved for this config. Apply default logic.
|
||||||
final String configModule = config['ftp_module'] ?? '';
|
final String configModule = config['ftp_module'] ?? '';
|
||||||
final bool isActive = (config['is_active'] == 1 || config['is_active'] == true);
|
// Robust check for active status
|
||||||
|
final activeVal = config['is_active'];
|
||||||
|
final bool isActive = activeVal == 1 || activeVal == true || activeVal.toString() == '1';
|
||||||
|
|
||||||
// Use the mapped key for comparison
|
// Use the mapped key for comparison
|
||||||
isEnabled = (configModule == expectedFtpModuleKey) && isActive;
|
isEnabled = (configModule == expectedFtpModuleKey) && isActive;
|
||||||
}
|
}
|
||||||
@ -248,7 +271,7 @@ class UserPreferencesService {
|
|||||||
/// destinations to send data to.
|
/// destinations to send data to.
|
||||||
Future<List<Map<String, dynamic>>> getEnabledApiConfigsForModule(String moduleName) async {
|
Future<List<Map<String, dynamic>>> getEnabledApiConfigsForModule(String moduleName) async {
|
||||||
// 1. Check the master switch for the module.
|
// 1. Check the master switch for the module.
|
||||||
final pref = await _dbHelper.getModulePreference(moduleName); // Use direct DB call
|
final pref = await getModulePreference(moduleName); // Use method that has default
|
||||||
if (pref == null || !(pref['is_api_enabled'] as bool)) {
|
if (pref == null || !(pref['is_api_enabled'] as bool)) {
|
||||||
debugPrint("API submissions are disabled for module '$moduleName'.");
|
debugPrint("API submissions are disabled for module '$moduleName'.");
|
||||||
return []; // Return empty list if API is disabled or not set.
|
return []; // Return empty list if API is disabled or not set.
|
||||||
@ -266,7 +289,7 @@ class UserPreferencesService {
|
|||||||
|
|
||||||
/// Retrieves only the FTP configurations that are actively enabled for a given module.
|
/// Retrieves only the FTP configurations that are actively enabled for a given module.
|
||||||
Future<List<Map<String, dynamic>>> getEnabledFtpConfigsForModule(String moduleName) async {
|
Future<List<Map<String, dynamic>>> getEnabledFtpConfigsForModule(String moduleName) async {
|
||||||
final pref = await _dbHelper.getModulePreference(moduleName); // Use direct DB call
|
final pref = await getModulePreference(moduleName); // Use method that has default
|
||||||
if (pref == null || !(pref['is_ftp_enabled'] as bool)) {
|
if (pref == null || !(pref['is_ftp_enabled'] as bool)) {
|
||||||
debugPrint("FTP submissions are disabled for module '$moduleName'.");
|
debugPrint("FTP submissions are disabled for module '$moduleName'.");
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@ -75,4 +75,4 @@ flutter:
|
|||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android: true
|
android: true
|
||||||
ios: true
|
ios: true
|
||||||
image_path: "assets/icon_3_512x512.png"
|
image_path: "assets/icon_4_512x512.png"
|
||||||