repair river module screen for in situ
This commit is contained in:
parent
d2b3ca2bb0
commit
70bf72feaf
@ -27,18 +27,15 @@ class RiverInSituSamplingData {
|
|||||||
|
|
||||||
// --- Step 2: Site Info & Photos ---
|
// --- Step 2: Site Info & Photos ---
|
||||||
String? weather;
|
String? weather;
|
||||||
// CHANGED: Renamed for river context
|
|
||||||
String? waterLevel;
|
|
||||||
String? riverCondition;
|
|
||||||
String? eventRemarks;
|
String? eventRemarks;
|
||||||
String? labRemarks;
|
String? labRemarks;
|
||||||
|
|
||||||
// CHANGED: Image descriptions adapted for river context
|
File? backgroundStationImage;
|
||||||
File? leftBankViewImage;
|
File? upstreamRiverImage;
|
||||||
File? rightBankViewImage;
|
File? downstreamRiverImage;
|
||||||
File? waterFillingImage;
|
|
||||||
File? waterColorImage;
|
// --- Step 4: Additional Photos ---
|
||||||
File? phPaperImage;
|
File? sampleTurbidityImage;
|
||||||
|
|
||||||
File? optionalImage1;
|
File? optionalImage1;
|
||||||
String? optionalRemark1;
|
String? optionalRemark1;
|
||||||
@ -64,12 +61,6 @@ class RiverInSituSamplingData {
|
|||||||
double? tss;
|
double? tss;
|
||||||
double? batteryVoltage;
|
double? batteryVoltage;
|
||||||
|
|
||||||
// --- START: Add your river-specific parameters here ---
|
|
||||||
// Example:
|
|
||||||
// double? flowRate;
|
|
||||||
// --- END: Add your river-specific parameters here ---
|
|
||||||
|
|
||||||
|
|
||||||
// --- Post-Submission Status ---
|
// --- Post-Submission Status ---
|
||||||
String? submissionStatus;
|
String? submissionStatus;
|
||||||
String? submissionMessage;
|
String? submissionMessage;
|
||||||
@ -80,6 +71,69 @@ class RiverInSituSamplingData {
|
|||||||
this.samplingTime,
|
this.samplingTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// ADDED: Factory constructor to create an instance from a map (JSON).
|
||||||
|
/// This is the required fix for the "fromJson isn't defined" error.
|
||||||
|
factory RiverInSituSamplingData.fromJson(Map<String, dynamic> json) {
|
||||||
|
// Helper function to safely create a File object from a path string
|
||||||
|
File? fileFromJson(dynamic path) {
|
||||||
|
return (path is String && path.isNotEmpty) ? File(path) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to safely parse numbers
|
||||||
|
double? doubleFromJson(dynamic value) {
|
||||||
|
if (value is num) return value.toDouble();
|
||||||
|
if (value is String) return double.tryParse(value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RiverInSituSamplingData()
|
||||||
|
..firstSamplerName = json['first_sampler_name']
|
||||||
|
..firstSamplerUserId = json['first_sampler_user_id']
|
||||||
|
..secondSampler = json['secondSampler']
|
||||||
|
..samplingDate = json['r_man_date']
|
||||||
|
..samplingTime = json['r_man_time']
|
||||||
|
..samplingType = json['r_man_type']
|
||||||
|
..sampleIdCode = json['r_man_sample_id_code']
|
||||||
|
..selectedStateName = json['selectedStateName']
|
||||||
|
..selectedCategoryName = json['selectedCategoryName']
|
||||||
|
..selectedStation = json['selectedStation']
|
||||||
|
..stationLatitude = json['stationLatitude']
|
||||||
|
..stationLongitude = json['stationLongitude']
|
||||||
|
..currentLatitude = json['r_man_current_latitude']?.toString()
|
||||||
|
..currentLongitude = json['r_man_current_longitude']?.toString()
|
||||||
|
..distanceDifferenceInKm = doubleFromJson(json['r_man_distance_difference'])
|
||||||
|
..distanceDifferenceRemarks = json['r_man_distance_difference_remarks']
|
||||||
|
..weather = json['r_man_weather']
|
||||||
|
..eventRemarks = json['r_man_event_remark']
|
||||||
|
..labRemarks = json['r_man_lab_remark']
|
||||||
|
..sondeId = json['r_man_sondeID']
|
||||||
|
..dataCaptureDate = json['data_capture_date']
|
||||||
|
..dataCaptureTime = json['data_capture_time']
|
||||||
|
..oxygenConcentration = doubleFromJson(json['r_man_oxygen_conc'])
|
||||||
|
..oxygenSaturation = doubleFromJson(json['r_man_oxygen_sat'])
|
||||||
|
..ph = doubleFromJson(json['r_man_ph'])
|
||||||
|
..salinity = doubleFromJson(json['r_man_salinity'])
|
||||||
|
..electricalConductivity = doubleFromJson(json['r_man_conductivity'])
|
||||||
|
..temperature = doubleFromJson(json['r_man_temperature'])
|
||||||
|
..tds = doubleFromJson(json['r_man_tds'])
|
||||||
|
..turbidity = doubleFromJson(json['r_man_turbidity'])
|
||||||
|
..tss = doubleFromJson(json['r_man_tss'])
|
||||||
|
..batteryVoltage = doubleFromJson(json['r_man_battery_volt'])
|
||||||
|
..optionalRemark1 = json['r_man_optional_photo_01_remarks']
|
||||||
|
..optionalRemark2 = json['r_man_optional_photo_02_remarks']
|
||||||
|
..optionalRemark3 = json['r_man_optional_photo_03_remarks']
|
||||||
|
..optionalRemark4 = json['r_man_optional_photo_04_remarks']
|
||||||
|
..backgroundStationImage = fileFromJson(json['r_man_background_station'])
|
||||||
|
..upstreamRiverImage = fileFromJson(json['r_man_upstream_river'])
|
||||||
|
..downstreamRiverImage = fileFromJson(json['r_man_downstream_river'])
|
||||||
|
..sampleTurbidityImage = fileFromJson(json['r_man_sample_turbidity'])
|
||||||
|
..optionalImage1 = fileFromJson(json['r_man_optional_photo_01'])
|
||||||
|
..optionalImage2 = fileFromJson(json['r_man_optional_photo_02'])
|
||||||
|
..optionalImage3 = fileFromJson(json['r_man_optional_photo_03'])
|
||||||
|
..optionalImage4 = fileFromJson(json['r_man_optional_photo_04']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Converts the data model into a Map<String, String> for the API form data.
|
/// Converts the data model into a Map<String, String> for the API form data.
|
||||||
Map<String, String> toApiFormData() {
|
Map<String, String> toApiFormData() {
|
||||||
final Map<String, String> map = {};
|
final Map<String, String> map = {};
|
||||||
@ -90,9 +144,6 @@ class RiverInSituSamplingData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT: The keys below are prefixed with 'r_man_' for river manual sampling.
|
|
||||||
// Ensure these match your backend API requirements.
|
|
||||||
|
|
||||||
// Step 1 Data
|
// Step 1 Data
|
||||||
add('first_sampler_user_id', firstSamplerUserId);
|
add('first_sampler_user_id', firstSamplerUserId);
|
||||||
add('r_man_second_sampler_id', secondSampler?['user_id']);
|
add('r_man_second_sampler_id', secondSampler?['user_id']);
|
||||||
@ -108,10 +159,10 @@ class RiverInSituSamplingData {
|
|||||||
|
|
||||||
// Step 2 Data
|
// Step 2 Data
|
||||||
add('r_man_weather', weather);
|
add('r_man_weather', weather);
|
||||||
add('r_man_water_level', waterLevel);
|
|
||||||
add('r_man_river_condition', riverCondition);
|
|
||||||
add('r_man_event_remark', eventRemarks);
|
add('r_man_event_remark', eventRemarks);
|
||||||
add('r_man_lab_remark', labRemarks);
|
add('r_man_lab_remark', labRemarks);
|
||||||
|
|
||||||
|
// Step 4 Data
|
||||||
add('r_man_optional_photo_01_remarks', optionalRemark1);
|
add('r_man_optional_photo_01_remarks', optionalRemark1);
|
||||||
add('r_man_optional_photo_02_remarks', optionalRemark2);
|
add('r_man_optional_photo_02_remarks', optionalRemark2);
|
||||||
add('r_man_optional_photo_03_remarks', optionalRemark3);
|
add('r_man_optional_photo_03_remarks', optionalRemark3);
|
||||||
@ -132,29 +183,22 @@ class RiverInSituSamplingData {
|
|||||||
add('r_man_tss', tss);
|
add('r_man_tss', tss);
|
||||||
add('r_man_battery_volt', batteryVoltage);
|
add('r_man_battery_volt', batteryVoltage);
|
||||||
|
|
||||||
// --- START: Add your new river parameters to the form data map ---
|
// Additional data for display or logging
|
||||||
// Example:
|
|
||||||
// add('r_man_flow_rate', flowRate);
|
|
||||||
// --- END: Add your new river parameters to the form data map ---
|
|
||||||
|
|
||||||
// Additional data for display or logging, adapted from the original model
|
|
||||||
add('first_sampler_name', firstSamplerName);
|
add('first_sampler_name', firstSamplerName);
|
||||||
// Assuming river station keys are prefixed with 'r_man_'
|
add('r_man_station_code', selectedStation?['sampling_station_code']);
|
||||||
add('r_man_station_code', selectedStation?['r_man_station_code']);
|
add('r_man_station_name', selectedStation?['sampling_river']);
|
||||||
add('r_man_station_name', selectedStation?['r_man_station_name']);
|
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts the image properties into a Map<String, File?> for the multipart API request.
|
/// Converts the image properties into a Map<String, File?> for the multipart API request.
|
||||||
Map<String, File?> toApiImageFiles() {
|
Map<String, File?> toApiImageFiles() {
|
||||||
// IMPORTANT: Keys adapted for river context.
|
|
||||||
return {
|
return {
|
||||||
'r_man_left_bank_view': leftBankViewImage,
|
'r_man_background_station': backgroundStationImage,
|
||||||
'r_man_right_bank_view': rightBankViewImage,
|
'r_man_upstream_river': upstreamRiverImage,
|
||||||
'r_man_filling_water_into_sample_bottle': waterFillingImage,
|
'r_man_downstream_river': downstreamRiverImage,
|
||||||
'r_man_water_in_clear_glass_bottle': waterColorImage,
|
'r_man_sample_turbidity': sampleTurbidityImage,
|
||||||
'r_man_examine_preservative_ph_paper': phPaperImage,
|
|
||||||
'r_man_optional_photo_01': optionalImage1,
|
'r_man_optional_photo_01': optionalImage1,
|
||||||
'r_man_optional_photo_02': optionalImage2,
|
'r_man_optional_photo_02': optionalImage2,
|
||||||
'r_man_optional_photo_03': optionalImage3,
|
'r_man_optional_photo_03': optionalImage3,
|
||||||
|
|||||||
@ -1,17 +1,13 @@
|
|||||||
// lib/screens/river/manual/widgets/data_status_log.dart
|
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
// CHANGED: Import River-specific models and services
|
|
||||||
import '../../../../models/river_in_situ_sampling_data.dart';
|
import '../../../../models/river_in_situ_sampling_data.dart';
|
||||||
import '../../../../services/local_storage_service.dart';
|
import '../../../../services/local_storage_service.dart';
|
||||||
import '../../../../services/river_api_service.dart';
|
import '../../../../services/river_api_service.dart';
|
||||||
|
|
||||||
// A unified model to represent any type of submission log entry.
|
|
||||||
class SubmissionLogEntry {
|
class SubmissionLogEntry {
|
||||||
final String type; // e.g., 'in-situ'
|
final String type;
|
||||||
final String title;
|
final String title;
|
||||||
final String stationCode;
|
final String stationCode;
|
||||||
final DateTime submissionDateTime;
|
final DateTime submissionDateTime;
|
||||||
@ -34,169 +30,134 @@ class SubmissionLogEntry {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// CHANGED: Renamed widget for River context
|
|
||||||
class RiverDataStatusLog extends StatefulWidget {
|
class RiverDataStatusLog extends StatefulWidget {
|
||||||
const RiverDataStatusLog({super.key});
|
const RiverDataStatusLog({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// CHANGED: Renamed state class
|
|
||||||
State<RiverDataStatusLog> createState() => _RiverDataStatusLogState();
|
State<RiverDataStatusLog> createState() => _RiverDataStatusLogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// CHANGED: Renamed state class
|
|
||||||
class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
||||||
final LocalStorageService _localStorageService = LocalStorageService();
|
final LocalStorageService _localStorageService = LocalStorageService();
|
||||||
// CHANGED: Use RiverApiService
|
|
||||||
final RiverApiService _riverApiService = RiverApiService();
|
final RiverApiService _riverApiService = RiverApiService();
|
||||||
|
|
||||||
Map<String, List<SubmissionLogEntry>> _groupedLogs = {};
|
// Raw data lists
|
||||||
Map<String, List<SubmissionLogEntry>> _filteredLogs = {};
|
List<SubmissionLogEntry> _scheduleLogs = [];
|
||||||
final Map<String, bool> _isCategoryExpanded = {};
|
List<SubmissionLogEntry> _triennialLogs = [];
|
||||||
|
List<SubmissionLogEntry> _otherLogs = [];
|
||||||
|
|
||||||
|
// Filtered lists for the UI
|
||||||
|
List<SubmissionLogEntry> _filteredScheduleLogs = [];
|
||||||
|
List<SubmissionLogEntry> _filteredTriennialLogs = [];
|
||||||
|
List<SubmissionLogEntry> _filteredOtherLogs = [];
|
||||||
|
|
||||||
|
// Per-category search controllers
|
||||||
|
final Map<String, TextEditingController> _searchControllers = {};
|
||||||
|
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
final TextEditingController _searchController = TextEditingController();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadAllLogs();
|
_loadAllLogs();
|
||||||
_searchController.addListener(_filterLogs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_searchController.dispose();
|
for (var controller in _searchControllers.values) {
|
||||||
|
controller.dispose();
|
||||||
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads logs for the river in-situ module.
|
|
||||||
Future<void> _loadAllLogs() async {
|
Future<void> _loadAllLogs() async {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
// NOTE: Assumes a method exists in your local storage service for river logs.
|
final riverLogs = await _localStorageService.getAllRiverInSituLogs();
|
||||||
final inSituLogs = await _localStorageService.getAllRiverInSituLogs();
|
|
||||||
|
|
||||||
final Map<String, List<SubmissionLogEntry>> tempGroupedLogs = {};
|
final List<SubmissionLogEntry> tempSchedule = [];
|
||||||
|
final List<SubmissionLogEntry> tempTriennial = [];
|
||||||
|
final List<SubmissionLogEntry> tempOthers = [];
|
||||||
|
|
||||||
// REMOVED: The entire block for fetching and mapping Tarball logs was here.
|
for (var log in riverLogs) {
|
||||||
|
final entry = SubmissionLogEntry(
|
||||||
// Map In-Situ logs for River
|
type: log['r_man_type'] as String? ?? 'Others',
|
||||||
final List<SubmissionLogEntry> inSituEntries = [];
|
title: log['selectedStation']?['sampling_river'] ?? 'Unknown Station',
|
||||||
for (var log in inSituLogs) {
|
stationCode: log['selectedStation']?['sampling_station_code'] ?? 'N/A',
|
||||||
final String dateStr = log['data_capture_date'] ?? log['sampling_date'] ?? '';
|
submissionDateTime: DateTime.tryParse('${log['r_man_date']} ${log['r_man_time']}') ?? DateTime.now(),
|
||||||
final String timeStr = log['data_capture_time'] ?? log['sampling_time'] ?? '';
|
|
||||||
|
|
||||||
inSituEntries.add(SubmissionLogEntry(
|
|
||||||
type: 'In-Situ Sampling',
|
|
||||||
// CHANGED: Use river-specific station keys
|
|
||||||
title: log['selectedStation']?['r_man_station_name'] ?? 'Unknown Station',
|
|
||||||
stationCode: log['selectedStation']?['r_man_station_code'] ?? 'N/A',
|
|
||||||
submissionDateTime: DateTime.tryParse('$dateStr $timeStr') ?? DateTime.now(),
|
|
||||||
reportId: log['reportId']?.toString(),
|
reportId: log['reportId']?.toString(),
|
||||||
status: log['submissionStatus'] ?? 'L1',
|
status: log['submissionStatus'] ?? 'L1',
|
||||||
message: log['submissionMessage'] ?? 'No status message.',
|
message: log['submissionMessage'] ?? 'No status message.',
|
||||||
rawData: log,
|
rawData: log,
|
||||||
));
|
);
|
||||||
|
|
||||||
|
switch (entry.type) {
|
||||||
|
case 'Schedule':
|
||||||
|
tempSchedule.add(entry);
|
||||||
|
break;
|
||||||
|
case 'Triennial':
|
||||||
|
tempTriennial.add(entry);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
tempOthers.add(entry);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (inSituEntries.isNotEmpty) {
|
|
||||||
inSituEntries.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
|
|
||||||
tempGroupedLogs['In-Situ Sampling'] = inSituEntries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tempSchedule.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
|
||||||
|
tempTriennial.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
|
||||||
|
tempOthers.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
|
||||||
|
|
||||||
|
// Initialize search controllers for categories that have data
|
||||||
|
final categories = {'Schedule': tempSchedule, 'Triennial': tempTriennial, 'Others': tempOthers};
|
||||||
|
categories.forEach((key, value) {
|
||||||
|
if (value.isNotEmpty) {
|
||||||
|
_searchControllers.putIfAbsent(key, () {
|
||||||
|
final controller = TextEditingController();
|
||||||
|
controller.addListener(() => _filterLogs());
|
||||||
|
return controller;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_groupedLogs = tempGroupedLogs;
|
_scheduleLogs = tempSchedule;
|
||||||
_filteredLogs = tempGroupedLogs;
|
_triennialLogs = tempTriennial;
|
||||||
|
_otherLogs = tempOthers;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
_filterLogs(); // Perform initial filter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _filterLogs() {
|
void _filterLogs() {
|
||||||
final query = _searchController.text.toLowerCase();
|
final scheduleQuery = _searchControllers['Schedule']?.text.toLowerCase() ?? '';
|
||||||
final Map<String, List<SubmissionLogEntry>> tempFiltered = {};
|
final triennialQuery = _searchControllers['Triennial']?.text.toLowerCase() ?? '';
|
||||||
|
final otherQuery = _searchControllers['Others']?.text.toLowerCase() ?? '';
|
||||||
|
|
||||||
_groupedLogs.forEach((category, logs) {
|
setState(() {
|
||||||
final filtered = logs.where((log) {
|
_filteredScheduleLogs = _scheduleLogs.where((log) => _logMatchesQuery(log, scheduleQuery)).toList();
|
||||||
|
_filteredTriennialLogs = _triennialLogs.where((log) => _logMatchesQuery(log, triennialQuery)).toList();
|
||||||
|
_filteredOtherLogs = _otherLogs.where((log) => _logMatchesQuery(log, otherQuery)).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _logMatchesQuery(SubmissionLogEntry log, String query) {
|
||||||
|
if (query.isEmpty) return true;
|
||||||
return log.title.toLowerCase().contains(query) ||
|
return log.title.toLowerCase().contains(query) ||
|
||||||
log.stationCode.toLowerCase().contains(query) ||
|
log.stationCode.toLowerCase().contains(query) ||
|
||||||
(log.reportId?.toLowerCase() ?? '').contains(query);
|
(log.reportId?.toLowerCase() ?? '').contains(query);
|
||||||
}).toList();
|
|
||||||
|
|
||||||
if (filtered.isNotEmpty) {
|
|
||||||
tempFiltered[category] = filtered;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_filteredLogs = tempFiltered;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Main router for resubmitting data based on its type.
|
|
||||||
Future<void> _resubmitData(SubmissionLogEntry log) async {
|
Future<void> _resubmitData(SubmissionLogEntry log) async {
|
||||||
setState(() => log.isResubmitting = true);
|
setState(() => log.isResubmitting = true);
|
||||||
|
|
||||||
switch (log.type) {
|
|
||||||
// REMOVED: The case for 'Tarball Sampling' was here.
|
|
||||||
case 'In-Situ Sampling':
|
|
||||||
await _resubmitInSituData(log);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text("Resubmission for '${log.type}' is not implemented."), backgroundColor: Colors.orange),
|
|
||||||
);
|
|
||||||
setState(() => log.isResubmitting = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// REMOVED: The entire _resubmitTarballData method was here.
|
|
||||||
|
|
||||||
/// Handles resubmission for River In-Situ data.
|
|
||||||
Future<void> _resubmitInSituData(SubmissionLogEntry log) async {
|
|
||||||
final logData = log.rawData;
|
final logData = log.rawData;
|
||||||
|
final dataToResubmit = RiverInSituSamplingData.fromJson(logData);
|
||||||
// CHANGED: Reconstruct the RiverInSituSamplingData object
|
|
||||||
final RiverInSituSamplingData dataToResubmit = RiverInSituSamplingData()
|
|
||||||
..firstSamplerUserId = int.tryParse(logData['first_sampler_user_id']?.toString() ?? '')
|
|
||||||
..secondSampler = logData['secondSampler']
|
|
||||||
..samplingDate = logData['sampling_date']
|
|
||||||
..samplingTime = logData['sampling_time']
|
|
||||||
..samplingType = logData['sampling_type']
|
|
||||||
..sampleIdCode = logData['sample_id_code']
|
|
||||||
..selectedStation = logData['selectedStation']
|
|
||||||
..currentLatitude = logData['current_latitude']?.toString()
|
|
||||||
..currentLongitude = logData['current_longitude']?.toString()
|
|
||||||
..distanceDifferenceInKm = double.tryParse(logData['distance_difference']?.toString() ?? '0.0')
|
|
||||||
..weather = logData['weather']
|
|
||||||
// CHANGED: Use river-specific fields
|
|
||||||
..waterLevel = logData['water_level']
|
|
||||||
..riverCondition = logData['river_condition']
|
|
||||||
..eventRemarks = logData['event_remarks']
|
|
||||||
..labRemarks = logData['lab_remarks']
|
|
||||||
..optionalRemark1 = logData['optional_photo_remark_1']
|
|
||||||
..optionalRemark2 = logData['optional_photo_remark_2']
|
|
||||||
..optionalRemark3 = logData['optional_photo_remark_3']
|
|
||||||
..optionalRemark4 = logData['optional_photo_remark_4']
|
|
||||||
..sondeId = logData['sonde_id']
|
|
||||||
..dataCaptureDate = logData['data_capture_date']
|
|
||||||
..dataCaptureTime = logData['data_capture_time']
|
|
||||||
..oxygenConcentration = double.tryParse(logData['oxygen_concentration_mg_l']?.toString() ?? '0.0')
|
|
||||||
..oxygenSaturation = double.tryParse(logData['oxygen_saturation_percent']?.toString() ?? '0.0')
|
|
||||||
..ph = double.tryParse(logData['ph']?.toString() ?? '0.0')
|
|
||||||
..salinity = double.tryParse(logData['salinity_ppt']?.toString() ?? '0.0')
|
|
||||||
..electricalConductivity = double.tryParse(logData['ec_us_cm']?.toString() ?? '0.0')
|
|
||||||
..temperature = double.tryParse(logData['temperature_c']?.toString() ?? '0.0')
|
|
||||||
..tds = double.tryParse(logData['tds_mg_l']?.toString() ?? '0.0')
|
|
||||||
..turbidity = double.tryParse(logData['turbidity_ntu']?.toString() ?? '0.0')
|
|
||||||
..tss = double.tryParse(logData['tss_mg_l']?.toString() ?? '0.0')
|
|
||||||
..batteryVoltage = double.tryParse(logData['battery_v']?.toString() ?? '0.0');
|
|
||||||
|
|
||||||
final Map<String, File?> imageFiles = {};
|
final Map<String, File?> imageFiles = {};
|
||||||
final imageKeys = dataToResubmit.toApiImageFiles().keys;
|
|
||||||
for (var key in imageKeys) {
|
final imageApiKeys = dataToResubmit.toApiImageFiles().keys;
|
||||||
|
for (var key in imageApiKeys) {
|
||||||
final imagePath = logData[key];
|
final imagePath = logData[key];
|
||||||
if (imagePath is String && imagePath.isNotEmpty) {
|
if (imagePath is String && imagePath.isNotEmpty) {
|
||||||
final file = File(imagePath);
|
final file = File(imagePath);
|
||||||
@ -206,88 +167,92 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CHANGED: Submit the data via the RiverApiService
|
|
||||||
final result = await _riverApiService.submitInSituSample(
|
final result = await _riverApiService.submitInSituSample(
|
||||||
formData: dataToResubmit.toApiFormData(),
|
formData: dataToResubmit.toApiFormData(),
|
||||||
imageFiles: imageFiles,
|
imageFiles: imageFiles,
|
||||||
);
|
);
|
||||||
|
|
||||||
logData['submissionStatus'] = result['status'];
|
logData['submissionStatus'] = result['status'];
|
||||||
logData['submissionMessage'] = result['message'];
|
logData['submissionMessage'] = result['message'];
|
||||||
logData['reportId'] = result['reportId']?.toString() ?? logData['reportId'];
|
logData['reportId'] = result['reportId']?.toString() ?? logData['reportId'];
|
||||||
|
|
||||||
// NOTE: Assumes a method exists to update river logs.
|
|
||||||
await _localStorageService.updateRiverInSituLog(logData);
|
await _localStorageService.updateRiverInSituLog(logData);
|
||||||
|
|
||||||
if (mounted) await _loadAllLogs();
|
if (mounted) await _loadAllLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final hasAnyLogs = _scheduleLogs.isNotEmpty || _triennialLogs.isNotEmpty || _otherLogs.isNotEmpty;
|
||||||
|
final hasFilteredLogs = _filteredScheduleLogs.isNotEmpty || _filteredTriennialLogs.isNotEmpty || _filteredOtherLogs.isNotEmpty;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// CHANGED: Updated AppBar title
|
|
||||||
appBar: AppBar(title: const Text('River Data Status Log')),
|
appBar: AppBar(title: const Text('River Data Status Log')),
|
||||||
body: Column(
|
body: _isLoading
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: TextField(
|
|
||||||
controller: _searchController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Search Logs...',
|
|
||||||
prefixIcon: const Icon(Icons.search),
|
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.0)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: _isLoading
|
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: RefreshIndicator(
|
: RefreshIndicator(
|
||||||
onRefresh: _loadAllLogs,
|
onRefresh: _loadAllLogs,
|
||||||
child: _filteredLogs.isEmpty
|
child: !hasAnyLogs
|
||||||
? Center(child: Text(_groupedLogs.isEmpty ? 'No submission logs found.' : 'No logs match your search.'))
|
? const Center(child: Text('No submission logs found.'))
|
||||||
: ListView(
|
: ListView(
|
||||||
children: _filteredLogs.entries.map((entry) {
|
padding: const EdgeInsets.all(8.0),
|
||||||
return _buildCategorySection(entry.key, entry.value);
|
children: [
|
||||||
}).toList(),
|
// No global search bar here
|
||||||
),
|
if (_scheduleLogs.isNotEmpty)
|
||||||
),
|
_buildCategorySection('Schedule', _filteredScheduleLogs),
|
||||||
|
if (_triennialLogs.isNotEmpty)
|
||||||
|
_buildCategorySection('Triennial', _filteredTriennialLogs),
|
||||||
|
if (_otherLogs.isNotEmpty)
|
||||||
|
_buildCategorySection('Others', _filteredOtherLogs),
|
||||||
|
if (!hasFilteredLogs && hasAnyLogs)
|
||||||
|
const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(24.0),
|
||||||
|
child: Text('No logs match your search.'),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCategorySection(String category, List<SubmissionLogEntry> logs) {
|
Widget _buildCategorySection(String category, List<SubmissionLogEntry> logs) {
|
||||||
final bool isExpanded = _isCategoryExpanded[category] ?? false;
|
// Calculate the height for the scrollable list.
|
||||||
final int itemCount = isExpanded ? logs.length : (logs.length > 5 ? 5 : logs.length);
|
// Each item is approx 75px high. Limit to 5 items height.
|
||||||
|
final listHeight = (logs.length > 5 ? 5.5 : logs.length.toDouble()) * 75.0;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(category, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
|
Text(category, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchControllers[category],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Search in $category...',
|
||||||
|
prefixIcon: const Icon(Icons.search, size: 20),
|
||||||
|
isDense: true,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
ListView.builder(
|
logs.isEmpty
|
||||||
|
? const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: Center(child: Text('No logs match your search in this category.')))
|
||||||
|
: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxHeight: listHeight),
|
||||||
|
child: ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
itemCount: logs.length,
|
||||||
itemCount: itemCount,
|
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return _buildLogListItem(logs[index]);
|
return _buildLogListItem(logs[index]);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (logs.length > 5)
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_isCategoryExpanded[category] = !isExpanded;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Text(isExpanded ? 'Show Less' : 'Show More (${logs.length - 5} more)'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -309,20 +274,12 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
|||||||
subtitle: Text(subtitle),
|
subtitle: Text(subtitle),
|
||||||
trailing: isFailed
|
trailing: isFailed
|
||||||
? (log.isResubmitting
|
? (log.isResubmitting
|
||||||
? const SizedBox(
|
? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 3))
|
||||||
height: 24,
|
: IconButton(icon: const Icon(Icons.sync, color: Colors.blue), tooltip: 'Resubmit', onPressed: () => _resubmitData(log)))
|
||||||
width: 24,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 3),
|
|
||||||
)
|
|
||||||
: IconButton(
|
|
||||||
icon: const Icon(Icons.sync, color: Colors.blue),
|
|
||||||
tooltip: 'Resubmit',
|
|
||||||
onPressed: () => _resubmitData(log),
|
|
||||||
))
|
|
||||||
: null,
|
: null,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@ -10,11 +10,10 @@ import '../../../services/local_storage_service.dart';
|
|||||||
import 'widgets/river_in_situ_step_1_sampling_info.dart';
|
import 'widgets/river_in_situ_step_1_sampling_info.dart';
|
||||||
import 'widgets/river_in_situ_step_2_site_info.dart';
|
import 'widgets/river_in_situ_step_2_site_info.dart';
|
||||||
import 'widgets/river_in_situ_step_3_data_capture.dart';
|
import 'widgets/river_in_situ_step_3_data_capture.dart';
|
||||||
import 'widgets/river_in_situ_step_4_summary.dart';
|
import 'widgets/river_in_situ_step_4_additional_info.dart';
|
||||||
|
import 'widgets/river_in_situ_step_5_summary.dart';
|
||||||
|
|
||||||
|
|
||||||
/// The main screen for the River In-Situ Sampling feature.
|
|
||||||
/// This stateful widget orchestrates the multi-step process using a PageView.
|
|
||||||
/// It manages the overall data model and the service layer for the entire workflow.
|
|
||||||
class RiverInSituSamplingScreen extends StatefulWidget {
|
class RiverInSituSamplingScreen extends StatefulWidget {
|
||||||
const RiverInSituSamplingScreen({super.key});
|
const RiverInSituSamplingScreen({super.key});
|
||||||
|
|
||||||
@ -27,10 +26,7 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
|||||||
|
|
||||||
late RiverInSituSamplingData _data;
|
late RiverInSituSamplingData _data;
|
||||||
|
|
||||||
// A single instance of the service to be used by all child widgets.
|
|
||||||
final RiverInSituSamplingService _samplingService = RiverInSituSamplingService();
|
final RiverInSituSamplingService _samplingService = RiverInSituSamplingService();
|
||||||
|
|
||||||
// Service for saving submission logs locally.
|
|
||||||
final LocalStorageService _localStorageService = LocalStorageService();
|
final LocalStorageService _localStorageService = LocalStorageService();
|
||||||
|
|
||||||
int _currentPage = 0;
|
int _currentPage = 0;
|
||||||
@ -39,8 +35,6 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Creates a NEW data object with the CURRENT date and time
|
|
||||||
// every time the user starts a new sampling.
|
|
||||||
_data = RiverInSituSamplingData(
|
_data = RiverInSituSamplingData(
|
||||||
samplingDate: DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
samplingDate: DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
||||||
samplingTime: DateFormat('HH:mm:ss').format(DateTime.now()),
|
samplingTime: DateFormat('HH:mm:ss').format(DateTime.now()),
|
||||||
@ -54,9 +48,8 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the next page in the form.
|
|
||||||
void _nextPage() {
|
void _nextPage() {
|
||||||
if (_currentPage < 3) {
|
if (_currentPage < 4) {
|
||||||
_pageController.nextPage(
|
_pageController.nextPage(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
@ -64,7 +57,6 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the previous page in the form.
|
|
||||||
void _previousPage() {
|
void _previousPage() {
|
||||||
if (_currentPage > 0) {
|
if (_currentPage > 0) {
|
||||||
_pageController.previousPage(
|
_pageController.previousPage(
|
||||||
@ -74,7 +66,6 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the final submission process.
|
|
||||||
Future<void> _submitForm() async {
|
Future<void> _submitForm() async {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
@ -86,7 +77,6 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
|||||||
_data.submissionMessage = result['message'];
|
_data.submissionMessage = result['message'];
|
||||||
_data.reportId = result['reportId']?.toString();
|
_data.reportId = result['reportId']?.toString();
|
||||||
|
|
||||||
// Save a log of the submission locally using the river-specific method.
|
|
||||||
await _localStorageService.saveRiverInSituSamplingData(_data);
|
await _localStorageService.saveRiverInSituSamplingData(_data);
|
||||||
|
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
@ -100,19 +90,18 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
|||||||
SnackBar(content: Text(message), backgroundColor: color, duration: const Duration(seconds: 4)),
|
SnackBar(content: Text(message), backgroundColor: color, duration: const Duration(seconds: 4)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result['status'] == 'L3') {
|
// ✅ CORRECTED: The navigation now happens regardless of the submission status.
|
||||||
|
// This ensures the form closes even after a failed (offline) submission.
|
||||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Use Provider.value to provide the existing river service instance to all child widgets.
|
|
||||||
return Provider.value(
|
return Provider.value(
|
||||||
value: _samplingService,
|
value: _samplingService,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('In-Situ Sampling (${_currentPage + 1}/4)'),
|
title: Text('In-Situ Sampling (${_currentPage + 1}/5)'),
|
||||||
leading: _currentPage > 0
|
leading: _currentPage > 0
|
||||||
? IconButton(
|
? IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
@ -129,11 +118,11 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
// Each step is a separate river-specific widget.
|
|
||||||
RiverInSituStep1SamplingInfo(data: _data, onNext: _nextPage),
|
RiverInSituStep1SamplingInfo(data: _data, onNext: _nextPage),
|
||||||
RiverInSituStep2SiteInfo(data: _data, onNext: _nextPage),
|
RiverInSituStep2SiteInfo(data: _data, onNext: _nextPage),
|
||||||
RiverInSituStep3DataCapture(data: _data, onNext: _nextPage),
|
RiverInSituStep3DataCapture(data: _data, onNext: _nextPage),
|
||||||
RiverInSituStep4Summary(data: _data, onSubmit: _submitForm, isLoading: _isLoading),
|
RiverInSituStep4AdditionalInfo(data: _data, onNext: _nextPage),
|
||||||
|
RiverInSituStep5Summary(data: _data, onSubmit: _submitForm, isLoading: _isLoading),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -36,10 +36,12 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
late final TextEditingController _stationLonController;
|
late final TextEditingController _stationLonController;
|
||||||
late final TextEditingController _currentLatController;
|
late final TextEditingController _currentLatController;
|
||||||
late final TextEditingController _currentLonController;
|
late final TextEditingController _currentLonController;
|
||||||
|
// REMOVED: Controllers for weather and remarks.
|
||||||
|
|
||||||
List<String> _statesList = [];
|
List<String> _statesList = [];
|
||||||
List<Map<String, dynamic>> _stationsForState = [];
|
List<Map<String, dynamic>> _stationsForState = [];
|
||||||
final List<String> _samplingTypes = ['Schedule', 'Ad-Hoc', 'Complaint'];
|
final List<String> _samplingTypes = ['Schedule', 'Triennial'];
|
||||||
|
// REMOVED: Weather options list.
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -58,6 +60,7 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
_stationLonController.dispose();
|
_stationLonController.dispose();
|
||||||
_currentLatController.dispose();
|
_currentLatController.dispose();
|
||||||
_currentLonController.dispose();
|
_currentLonController.dispose();
|
||||||
|
// REMOVED: Dispose controllers for remarks.
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +73,7 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
_stationLonController = TextEditingController(text: widget.data.stationLongitude);
|
_stationLonController = TextEditingController(text: widget.data.stationLongitude);
|
||||||
_currentLatController = TextEditingController(text: widget.data.currentLatitude);
|
_currentLatController = TextEditingController(text: widget.data.currentLatitude);
|
||||||
_currentLonController = TextEditingController(text: widget.data.currentLongitude);
|
_currentLonController = TextEditingController(text: widget.data.currentLongitude);
|
||||||
|
// REMOVED: Initialize controllers for remarks.
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initializeForm() {
|
void _initializeForm() {
|
||||||
@ -392,7 +396,11 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
|||||||
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"),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// REMOVED: On-Site Information section (Weather, Remarks).
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _goToNextStep,
|
onPressed: _goToNextStep,
|
||||||
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
|
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
// lib/screens/river/manual/widgets/river_in_situ_step_2_site_info.dart
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
@ -26,33 +28,19 @@ class _RiverInSituStep2SiteInfoState extends State<RiverInSituStep2SiteInfo> {
|
|||||||
|
|
||||||
late final TextEditingController _eventRemarksController;
|
late final TextEditingController _eventRemarksController;
|
||||||
late final TextEditingController _labRemarksController;
|
late final TextEditingController _labRemarksController;
|
||||||
late final TextEditingController _optionalRemark1Controller;
|
final List<String> _weatherOptions = ['Clear', 'Rainy', 'Cloudy'];
|
||||||
late final TextEditingController _optionalRemark2Controller;
|
|
||||||
late final TextEditingController _optionalRemark3Controller;
|
|
||||||
late final TextEditingController _optionalRemark4Controller;
|
|
||||||
|
|
||||||
final List<String> _weatherOptions = ['Clear', 'Rainy', 'Cloudy', 'Windy', 'Sunny', 'Drizzle'];
|
|
||||||
// MODIFIED: Removed _waterLevelOptions and _riverConditionOptions
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_eventRemarksController = TextEditingController(text: widget.data.eventRemarks);
|
_eventRemarksController = TextEditingController(text: widget.data.eventRemarks);
|
||||||
_labRemarksController = TextEditingController(text: widget.data.labRemarks);
|
_labRemarksController = TextEditingController(text: widget.data.labRemarks);
|
||||||
_optionalRemark1Controller = TextEditingController(text: widget.data.optionalRemark1);
|
|
||||||
_optionalRemark2Controller = TextEditingController(text: widget.data.optionalRemark2);
|
|
||||||
_optionalRemark3Controller = TextEditingController(text: widget.data.optionalRemark3);
|
|
||||||
_optionalRemark4Controller = TextEditingController(text: widget.data.optionalRemark4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_eventRemarksController.dispose();
|
_eventRemarksController.dispose();
|
||||||
_labRemarksController.dispose();
|
_labRemarksController.dispose();
|
||||||
_optionalRemark1Controller.dispose();
|
|
||||||
_optionalRemark2Controller.dispose();
|
|
||||||
_optionalRemark3Controller.dispose();
|
|
||||||
_optionalRemark4Controller.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,20 +67,16 @@ class _RiverInSituStep2SiteInfoState extends State<RiverInSituStep2SiteInfo> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.data.leftBankViewImage == null ||
|
_formKey.currentState!.save();
|
||||||
widget.data.rightBankViewImage == null ||
|
|
||||||
widget.data.waterFillingImage == null ||
|
// UPDATED: Validation now checks for 3 required photos.
|
||||||
widget.data.waterColorImage == null ||
|
if (widget.data.backgroundStationImage == null ||
|
||||||
widget.data.phPaperImage == null) {
|
widget.data.upstreamRiverImage == null ||
|
||||||
_showSnackBar('Please attach all 5 required photos before proceeding.', isError: true);
|
widget.data.downstreamRiverImage == null) {
|
||||||
|
_showSnackBar('Please attach all 3 required photos before proceeding.', isError: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_formKey.currentState!.save();
|
|
||||||
widget.data.optionalRemark1 = _optionalRemark1Controller.text;
|
|
||||||
widget.data.optionalRemark2 = _optionalRemark2Controller.text;
|
|
||||||
widget.data.optionalRemark3 = _optionalRemark3Controller.text;
|
|
||||||
widget.data.optionalRemark4 = _optionalRemark4Controller.text;
|
|
||||||
widget.onNext();
|
widget.onNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,39 +96,17 @@ class _RiverInSituStep2SiteInfoState extends State<RiverInSituStep2SiteInfo> {
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
children: [
|
children: [
|
||||||
Text("On-Site Information", style: Theme.of(context).textTheme.headlineSmall),
|
Text("On-Site Information", style: Theme.of(context).textTheme.titleLarge),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 16),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: widget.data.weather,
|
value: widget.data.weather,
|
||||||
items: _weatherOptions.map((item) => DropdownMenuItem(value: item, child: Text(item))).toList(),
|
items: _weatherOptions.map((item) => DropdownMenuItem(value: item, child: Text(item))).toList(),
|
||||||
onChanged: (value) => setState(() => widget.data.weather = value),
|
onChanged: (value) => setState(() => widget.data.weather = value),
|
||||||
decoration: const InputDecoration(labelText: 'Weather *'),
|
decoration: const InputDecoration(labelText: 'Weather *'),
|
||||||
validator: (value) => value == null ? 'Weather is required' : null,
|
validator: (value) => value == null ? 'Weather is required' : null,
|
||||||
|
onSaved: (value) => widget.data.weather = value,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// MODIFIED: The DropdownButtonFormField for 'Water Level' has been removed.
|
|
||||||
// MODIFIED: The DropdownButtonFormField for 'River Condition' has been removed.
|
|
||||||
|
|
||||||
Text("Required Photos *", style: Theme.of(context).textTheme.titleLarge),
|
|
||||||
const Text("All photos must be taken in landscape (horizontal) orientation.", style: TextStyle(color: Colors.grey)),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildImagePicker('Left Bank View', 'LEFT_BANK_VIEW', widget.data.leftBankViewImage, (file) => widget.data.leftBankViewImage = file, isRequired: true),
|
|
||||||
_buildImagePicker('Right Bank View', 'RIGHT_BANK_VIEW', widget.data.rightBankViewImage, (file) => widget.data.rightBankViewImage = file, isRequired: true),
|
|
||||||
_buildImagePicker('Filling Water into Sample Bottle', 'WATER_FILLING', widget.data.waterFillingImage, (file) => widget.data.waterFillingImage = file, isRequired: true),
|
|
||||||
_buildImagePicker('Water in Clear Glass Bottle', 'WATER_COLOR', widget.data.waterColorImage, (file) => widget.data.waterColorImage = file, isRequired: true),
|
|
||||||
_buildImagePicker('Examine Preservative (pH paper)', 'PH_PAPER', widget.data.phPaperImage, (file) => widget.data.phPaperImage = file, isRequired: true),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
Text("Optional Photos & Remarks", style: Theme.of(context).textTheme.titleLarge),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildImagePicker('Optional Photo 1', 'OPTIONAL_1', widget.data.optionalImage1, (file) => widget.data.optionalImage1 = file, remarkController: _optionalRemark1Controller, isRequired: false),
|
|
||||||
_buildImagePicker('Optional Photo 2', 'OPTIONAL_2', widget.data.optionalImage2, (file) => widget.data.optionalImage2 = file, remarkController: _optionalRemark2Controller, isRequired: false),
|
|
||||||
_buildImagePicker('Optional Photo 3', 'OPTIONAL_3', widget.data.optionalImage3, (file) => widget.data.optionalImage3 = file, remarkController: _optionalRemark3Controller, isRequired: false),
|
|
||||||
_buildImagePicker('Optional Photo 4', 'OPTIONAL_4', widget.data.optionalImage4, (file) => widget.data.optionalImage4 = file, remarkController: _optionalRemark4Controller, isRequired: false),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
Text("Remarks", style: Theme.of(context).textTheme.titleLarge),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _eventRemarksController,
|
controller: _eventRemarksController,
|
||||||
decoration: const InputDecoration(labelText: 'Event Remarks (Optional)', hintText: 'e.g., unusual smells, colors, etc.'),
|
decoration: const InputDecoration(labelText: 'Event Remarks (Optional)', hintText: 'e.g., unusual smells, colors, etc.'),
|
||||||
@ -158,6 +120,20 @@ class _RiverInSituStep2SiteInfoState extends State<RiverInSituStep2SiteInfo> {
|
|||||||
onSaved: (value) => widget.data.labRemarks = value,
|
onSaved: (value) => widget.data.labRemarks = value,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
),
|
),
|
||||||
|
const Divider(height: 32),
|
||||||
|
|
||||||
|
Text("Required Photos *", style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const Text("All photos must be taken in landscape (horizontal) orientation.", style: TextStyle(color: Colors.grey)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
_buildImagePicker('Background Station', 'BACKGROUND_STATION', widget.data.backgroundStationImage, (file) => widget.data.backgroundStationImage = file, isRequired: true),
|
||||||
|
_buildImagePicker('Upstream River', 'UPSTREAM_RIVER', widget.data.upstreamRiverImage, (file) => widget.data.upstreamRiverImage = file, isRequired: true),
|
||||||
|
_buildImagePicker('Downstream River', 'DOWNSTREAM_RIVER', widget.data.downstreamRiverImage, (file) => widget.data.downstreamRiverImage = file, isRequired: true),
|
||||||
|
|
||||||
|
// REMOVED: The "Sample Turbidity" image picker was here.
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _goToNextStep,
|
onPressed: _goToNextStep,
|
||||||
|
|||||||
@ -8,11 +8,12 @@ import 'package:usb_serial/usb_serial.dart';
|
|||||||
|
|
||||||
import '../../../../models/river_in_situ_sampling_data.dart';
|
import '../../../../models/river_in_situ_sampling_data.dart';
|
||||||
import '../../../../services/river_in_situ_sampling_service.dart';
|
import '../../../../services/river_in_situ_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';
|
||||||
|
|
||||||
|
// UPDATED: Class name changed from RiverInSituStep2DataCapture to RiverInSituStep3DataCapture
|
||||||
class RiverInSituStep3DataCapture extends StatefulWidget {
|
class RiverInSituStep3DataCapture extends StatefulWidget {
|
||||||
final RiverInSituSamplingData data;
|
final RiverInSituSamplingData data;
|
||||||
final VoidCallback onNext;
|
final VoidCallback onNext;
|
||||||
@ -24,9 +25,11 @@ class RiverInSituStep3DataCapture extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
// UPDATED: State class reference
|
||||||
State<RiverInSituStep3DataCapture> createState() => _RiverInSituStep3DataCaptureState();
|
State<RiverInSituStep3DataCapture> createState() => _RiverInSituStep3DataCaptureState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UPDATED: State class name
|
||||||
class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCapture> {
|
class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCapture> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
@ -48,7 +51,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
final _turbidityController = TextEditingController();
|
final _turbidityController = TextEditingController();
|
||||||
final _tssController = TextEditingController();
|
final _tssController = TextEditingController();
|
||||||
final _batteryController = TextEditingController();
|
final _batteryController = TextEditingController();
|
||||||
// NOTE: If you add river-specific parameters, add their controllers here.
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -81,7 +83,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
widget.data.turbidity ??= -999.0;
|
widget.data.turbidity ??= -999.0;
|
||||||
widget.data.tss ??= -999.0;
|
widget.data.tss ??= -999.0;
|
||||||
widget.data.batteryVoltage ??= -999.0;
|
widget.data.batteryVoltage ??= -999.0;
|
||||||
// NOTE: Initialize your river-specific parameters here (e.g., widget.data.flowRate ??= -999.0;)
|
|
||||||
|
|
||||||
_oxyConcController.text = widget.data.oxygenConcentration!.toString();
|
_oxyConcController.text = widget.data.oxygenConcentration!.toString();
|
||||||
_oxySatController.text = widget.data.oxygenSaturation!.toString();
|
_oxySatController.text = widget.data.oxygenSaturation!.toString();
|
||||||
@ -106,7 +107,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
{'icon': Icons.opacity, 'label': 'Turbidity', 'unit': 'NTU', 'controller': _turbidityController},
|
{'icon': Icons.opacity, 'label': 'Turbidity', 'unit': 'NTU', 'controller': _turbidityController},
|
||||||
{'icon': Icons.filter_alt_outlined, 'label': 'TSS', 'unit': 'mg/L', 'controller': _tssController},
|
{'icon': Icons.filter_alt_outlined, 'label': 'TSS', 'unit': 'mg/L', 'controller': _tssController},
|
||||||
{'icon': Icons.battery_charging_full, 'label': 'Battery', 'unit': 'V', 'controller': _batteryController},
|
{'icon': Icons.battery_charging_full, 'label': 'Battery', 'unit': 'V', 'controller': _batteryController},
|
||||||
// NOTE: If you add river-specific parameters, add them to this list.
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,27 +125,21 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
_turbidityController.dispose();
|
_turbidityController.dispose();
|
||||||
_tssController.dispose();
|
_tssController.dispose();
|
||||||
_batteryController.dispose();
|
_batteryController.dispose();
|
||||||
// NOTE: Dispose your river-specific controllers here.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleConnectionAttempt(String type) async {
|
Future<void> _handleConnectionAttempt(String type) async {
|
||||||
final service = context.read<RiverInSituSamplingService>();
|
final service = context.read<RiverInSituSamplingService>();
|
||||||
|
|
||||||
final bool hasPermissions = await service.requestDevicePermissions();
|
final bool hasPermissions = await service.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));
|
await Future.delayed(const Duration(milliseconds: 250));
|
||||||
|
|
||||||
final bool connectionSuccess = await _connectToDevice(type);
|
final bool connectionSuccess = await _connectToDevice(type);
|
||||||
|
|
||||||
if (connectionSuccess && mounted) {
|
if (connectionSuccess && mounted) {
|
||||||
_dataSubscription?.cancel();
|
_dataSubscription?.cancel();
|
||||||
final stream = type == 'bluetooth' ? service.bluetoothDataStream : service.serialDataStream;
|
final stream = type == 'bluetooth' ? service.bluetoothDataStream : service.serialDataStream;
|
||||||
|
|
||||||
_dataSubscription = stream.listen((readings) {
|
_dataSubscription = stream.listen((readings) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_updateTextFields(readings);
|
_updateTextFields(readings);
|
||||||
@ -158,7 +152,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
final service = context.read<RiverInSituSamplingService>();
|
final service = context.read<RiverInSituSamplingService>();
|
||||||
bool success = false;
|
bool success = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (type == 'bluetooth') {
|
if (type == 'bluetooth') {
|
||||||
final devices = await service.getPairedBluetoothDevices();
|
final devices = await service.getPairedBluetoothDevices();
|
||||||
@ -255,22 +248,14 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
title: const Text('Data Collection Active'),
|
title: const Text('Data Collection Active'),
|
||||||
content: const Text('Please stop the live data collection before proceeding.'),
|
content: const Text('Please stop the live data collection before proceeding.'),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(child: const Text('OK'), onPressed: () => Navigator.of(context).pop())
|
||||||
child: const Text('OK'),
|
]);
|
||||||
onPressed: () {
|
});
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_formKey.currentState!.validate()){
|
if (_formKey.currentState!.validate()){
|
||||||
_formKey.currentState!.save();
|
_formKey.currentState!.save();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const defaultValue = -999.0;
|
const defaultValue = -999.0;
|
||||||
widget.data.temperature = double.tryParse(_tempController.text) ?? defaultValue;
|
widget.data.temperature = double.tryParse(_tempController.text) ?? defaultValue;
|
||||||
@ -287,7 +272,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
_showSnackBar("Could not save parameters due to a data format error.", isError: true);
|
_showSnackBar("Could not save parameters due to a data format error.", isError: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
widget.onNext();
|
widget.onNext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -321,7 +305,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
return Form(
|
return Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
// CORRECTED: Scrolling is enabled by removing the physics property.
|
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
children: [
|
children: [
|
||||||
Text("Data Capture", style: Theme.of(context).textTheme.headlineSmall),
|
Text("Data Capture", style: Theme.of(context).textTheme.headlineSmall),
|
||||||
@ -370,10 +353,14 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
valueListenable: service.sondeId,
|
valueListenable: service.sondeId,
|
||||||
builder: (context, sondeId, child) {
|
builder: (context, sondeId, child) {
|
||||||
final newSondeId = sondeId ?? '';
|
final newSondeId = sondeId ?? '';
|
||||||
if (_sondeIdController.text != newSondeId) {
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted && _sondeIdController.text != newSondeId) {
|
||||||
_sondeIdController.text = newSondeId;
|
_sondeIdController.text = newSondeId;
|
||||||
widget.data.sondeId = newSondeId;
|
widget.data.sondeId = newSondeId;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: _sondeIdController,
|
controller: _sondeIdController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
@ -447,14 +434,12 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
Widget _buildConnectionCard({required String type, required dynamic connectionState, String? deviceName}) {
|
Widget _buildConnectionCard({required String type, required dynamic connectionState, String? deviceName}) {
|
||||||
final isConnected = connectionState == BluetoothConnectionState.connected || connectionState == SerialConnectionState.connected;
|
final isConnected = connectionState == BluetoothConnectionState.connected || connectionState == SerialConnectionState.connected;
|
||||||
final isConnecting = connectionState == BluetoothConnectionState.connecting || connectionState == SerialConnectionState.connecting;
|
final isConnecting = connectionState == BluetoothConnectionState.connecting || connectionState == SerialConnectionState.connecting;
|
||||||
|
|
||||||
Color statusColor = isConnected ? Colors.green : Colors.red;
|
Color statusColor = isConnected ? Colors.green : Colors.red;
|
||||||
String statusText = isConnected ? 'Connected to ${deviceName ?? 'device'}' : 'Disconnected';
|
String statusText = isConnected ? 'Connected to ${deviceName ?? 'device'}' : 'Disconnected';
|
||||||
if (isConnecting) {
|
if (isConnecting) {
|
||||||
statusColor = Colors.orange;
|
statusColor = Colors.orange;
|
||||||
statusText = 'Connecting...';
|
statusText = 'Connecting...';
|
||||||
}
|
}
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@ -0,0 +1,185 @@
|
|||||||
|
// lib/screens/river/manual/widgets/river_in_situ_step_4_additional_info.dart
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../../../models/river_in_situ_sampling_data.dart';
|
||||||
|
import '../../../../services/river_in_situ_sampling_service.dart';
|
||||||
|
|
||||||
|
class RiverInSituStep4AdditionalInfo extends StatefulWidget {
|
||||||
|
final RiverInSituSamplingData data;
|
||||||
|
final VoidCallback onNext;
|
||||||
|
|
||||||
|
const RiverInSituStep4AdditionalInfo({
|
||||||
|
super.key,
|
||||||
|
required this.data,
|
||||||
|
required this.onNext,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RiverInSituStep4AdditionalInfo> createState() =>
|
||||||
|
_RiverInSituStep4AdditionalInfoState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RiverInSituStep4AdditionalInfoState
|
||||||
|
extends State<RiverInSituStep4AdditionalInfo> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
bool _isPickingImage = false;
|
||||||
|
|
||||||
|
late final TextEditingController _optionalRemark1Controller;
|
||||||
|
late final TextEditingController _optionalRemark2Controller;
|
||||||
|
late final TextEditingController _optionalRemark3Controller;
|
||||||
|
late final TextEditingController _optionalRemark4Controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_optionalRemark1Controller = TextEditingController(text: widget.data.optionalRemark1);
|
||||||
|
_optionalRemark2Controller = TextEditingController(text: widget.data.optionalRemark2);
|
||||||
|
_optionalRemark3Controller = TextEditingController(text: widget.data.optionalRemark3);
|
||||||
|
_optionalRemark4Controller = TextEditingController(text: widget.data.optionalRemark4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_optionalRemark1Controller.dispose();
|
||||||
|
_optionalRemark2Controller.dispose();
|
||||||
|
_optionalRemark3Controller.dispose();
|
||||||
|
_optionalRemark4Controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setImage(Function(File?) setImageCallback, ImageSource source, String imageInfo, {required bool isRequired}) async {
|
||||||
|
if (_isPickingImage) return;
|
||||||
|
setState(() => _isPickingImage = true);
|
||||||
|
|
||||||
|
final service = Provider.of<RiverInSituSamplingService>(context, listen: false);
|
||||||
|
final file = await service.pickAndProcessImage(source, data: widget.data, imageInfo: imageInfo, isRequired: isRequired);
|
||||||
|
|
||||||
|
if (file != null) {
|
||||||
|
setState(() => setImageCallback(file));
|
||||||
|
} else if (mounted) {
|
||||||
|
_showSnackBar('Image selection failed. Please ensure all photos are taken in landscape mode.', isError: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isPickingImage = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _goToNextStep() {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
_formKey.currentState!.save();
|
||||||
|
|
||||||
|
// ADDED: Validation for the moved required photo.
|
||||||
|
if (widget.data.sampleTurbidityImage == null) {
|
||||||
|
_showSnackBar('Please attach the Sample Turbidity photo before proceeding.', isError: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.data.optionalRemark1 = _optionalRemark1Controller.text;
|
||||||
|
widget.data.optionalRemark2 = _optionalRemark2Controller.text;
|
||||||
|
widget.data.optionalRemark3 = _optionalRemark3Controller.text;
|
||||||
|
widget.data.optionalRemark4 = _optionalRemark4Controller.text;
|
||||||
|
widget.onNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSnackBar(String message, {bool isError = false}) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
backgroundColor: isError ? Colors.red : null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
children: [
|
||||||
|
Text("Additional Photos",
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// ADDED: The required "Sample Turbidity" photo moved from Step 2.
|
||||||
|
Text("Required Photo *", style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildImagePicker('Sample Turbidity', 'SAMPLE_TURBIDITY', widget.data.sampleTurbidityImage, (file) => widget.data.sampleTurbidityImage = file, isRequired: true),
|
||||||
|
|
||||||
|
const Divider(height: 32),
|
||||||
|
|
||||||
|
Text("Optional Photos & Remarks", style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildImagePicker('Optional Photo 1', 'OPTIONAL_1', widget.data.optionalImage1, (file) => widget.data.optionalImage1 = file, remarkController: _optionalRemark1Controller, isRequired: false),
|
||||||
|
_buildImagePicker('Optional Photo 2', 'OPTIONAL_2', widget.data.optionalImage2, (file) => widget.data.optionalImage2 = file, remarkController: _optionalRemark2Controller, isRequired: false),
|
||||||
|
_buildImagePicker('Optional Photo 3', 'OPTIONAL_3', widget.data.optionalImage3, (file) => widget.data.optionalImage3 = file, remarkController: _optionalRemark3Controller, isRequired: false),
|
||||||
|
_buildImagePicker('Optional Photo 4', 'OPTIONAL_4', widget.data.optionalImage4, (file) => widget.data.optionalImage4 = file, remarkController: _optionalRemark4Controller, isRequired: false),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _goToNextStep,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16)),
|
||||||
|
child: const Text('Next'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImagePicker(String title, String imageInfo, File? imageFile, Function(File?) setImageCallback, {TextEditingController? remarkController, bool isRequired = false}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(title + (isRequired ? ' *' : ''), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (imageFile != null)
|
||||||
|
Stack(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
children: [
|
||||||
|
ClipRRect(borderRadius: BorderRadius.circular(8.0), child: Image.file(imageFile, key: UniqueKey(), height: 150, width: double.infinity, fit: BoxFit.cover)),
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(color: Colors.black.withOpacity(0.6), shape: BoxShape.circle),
|
||||||
|
child: IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(Icons.close, color: Colors.white, size: 20),
|
||||||
|
onPressed: () => setState(() => setImageCallback(null)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
ElevatedButton.icon(onPressed: _isPickingImage ? null : () => _setImage(setImageCallback, ImageSource.camera, imageInfo, isRequired: isRequired), icon: const Icon(Icons.camera_alt), label: const Text("Camera")),
|
||||||
|
ElevatedButton.icon(onPressed: _isPickingImage ? null : () => _setImage(setImageCallback, ImageSource.gallery, imageInfo, isRequired: isRequired), icon: const Icon(Icons.photo_library), label: const Text("Gallery")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (remarkController != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: remarkController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Remarks for $title',
|
||||||
|
hintText: 'Add an optional remark...',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,19 +1,16 @@
|
|||||||
// lib/screens/river/manual/widgets/river_in_situ_step_4_summary.dart
|
// lib/screens/river/manual/widgets/river_in_situ_step_5_summary.dart
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
// CHANGED: Import river-specific data model
|
|
||||||
import '../../../../models/river_in_situ_sampling_data.dart';
|
import '../../../../models/river_in_situ_sampling_data.dart';
|
||||||
|
|
||||||
// CHANGED: Renamed class for river context
|
class RiverInSituStep5Summary extends StatelessWidget {
|
||||||
class RiverInSituStep4Summary extends StatelessWidget {
|
|
||||||
// CHANGED: Expects river-specific data model
|
|
||||||
final RiverInSituSamplingData data;
|
final RiverInSituSamplingData data;
|
||||||
final VoidCallback onSubmit;
|
final VoidCallback onSubmit;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
|
|
||||||
const RiverInSituStep4Summary({
|
const RiverInSituStep5Summary({
|
||||||
super.key,
|
super.key,
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
@ -44,45 +41,45 @@ class RiverInSituStep4Summary extends StatelessWidget {
|
|||||||
_buildDetailRow("Sample ID Code:", data.sampleIdCode),
|
_buildDetailRow("Sample ID Code:", data.sampleIdCode),
|
||||||
const Divider(height: 20),
|
const Divider(height: 20),
|
||||||
_buildDetailRow("State:", data.selectedStateName),
|
_buildDetailRow("State:", data.selectedStateName),
|
||||||
_buildDetailRow("Category:", data.selectedCategoryName),
|
_buildDetailRow(
|
||||||
// CHANGED: Use river-specific station keys
|
"Station:",
|
||||||
_buildDetailRow("Station Code:", data.selectedStation?['r_man_station_code']?.toString()),
|
"${data.selectedStation?['sampling_station_code']} | ${data.selectedStation?['sampling_river']} | ${data.selectedStation?['sampling_basin']}"
|
||||||
_buildDetailRow("Station Name:", data.selectedStation?['r_man_station_name']?.toString()),
|
),
|
||||||
_buildDetailRow("Station Location:", "${data.stationLatitude}, ${data.stationLongitude}"),
|
_buildDetailRow("Station Location:", "${data.stationLatitude}, ${data.stationLongitude}"),
|
||||||
|
// REMOVED: Weather and remarks moved to the next section.
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
_buildSectionCard(
|
_buildSectionCard(
|
||||||
context,
|
context,
|
||||||
"Location & On-Site Info",
|
"Site Info & Required Photos",
|
||||||
[
|
[
|
||||||
_buildDetailRow("Current Location:", "${data.currentLatitude}, ${data.currentLongitude}"),
|
_buildDetailRow("Current Location:", "${data.currentLatitude}, ${data.currentLongitude}"),
|
||||||
_buildDetailRow("Distance Difference:", data.distanceDifferenceInKm != null ? "${(data.distanceDifferenceInKm! * 1000).toStringAsFixed(0)} meters" : "N/A"),
|
_buildDetailRow("Distance Difference:", data.distanceDifferenceInKm != null ? "${(data.distanceDifferenceInKm! * 1000).toStringAsFixed(0)} meters" : "N/A"),
|
||||||
if (data.distanceDifferenceRemarks != null && data.distanceDifferenceRemarks!.isNotEmpty)
|
if (data.distanceDifferenceRemarks != null && data.distanceDifferenceRemarks!.isNotEmpty)
|
||||||
_buildDetailRow("Distance Remarks:", data.distanceDifferenceRemarks),
|
_buildDetailRow("Distance Remarks:", data.distanceDifferenceRemarks),
|
||||||
const Divider(height: 20),
|
const Divider(height: 20),
|
||||||
|
|
||||||
|
// ADDED: Display for Weather and Remarks.
|
||||||
_buildDetailRow("Weather:", data.weather),
|
_buildDetailRow("Weather:", data.weather),
|
||||||
// CHANGED: Use river-specific fields
|
|
||||||
_buildDetailRow("Water Level:", data.waterLevel),
|
|
||||||
_buildDetailRow("River Condition:", data.riverCondition),
|
|
||||||
_buildDetailRow("Event Remarks:", data.eventRemarks),
|
_buildDetailRow("Event Remarks:", data.eventRemarks),
|
||||||
_buildDetailRow("Lab Remarks:", data.labRemarks),
|
_buildDetailRow("Lab Remarks:", data.labRemarks),
|
||||||
|
const Divider(height: 20),
|
||||||
|
|
||||||
|
// UPDATED: Image cards reflect new names and data properties.
|
||||||
|
_buildImageCard("Background Station", data.backgroundStationImage),
|
||||||
|
_buildImageCard("Upstream River", data.upstreamRiverImage),
|
||||||
|
_buildImageCard("Downstream River", data.downstreamRiverImage),
|
||||||
|
_buildImageCard("Sample Turbidity", data.sampleTurbidityImage),
|
||||||
|
|
||||||
|
// REMOVED: pH paper image card.
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
_buildSectionCard(
|
_buildSectionCard(
|
||||||
context,
|
context,
|
||||||
"Attached Photos",
|
"Optional Photos & Remarks",
|
||||||
[
|
[
|
||||||
// CHANGED: Use river-specific image properties and labels
|
|
||||||
_buildImageCard("Left Bank View", data.leftBankViewImage),
|
|
||||||
_buildImageCard("Right Bank View", data.rightBankViewImage),
|
|
||||||
_buildImageCard("Filling Water into Bottle", data.waterFillingImage),
|
|
||||||
_buildImageCard("Water Color in Bottle", data.waterColorImage),
|
|
||||||
_buildImageCard("Examine Preservative (pH paper)", data.phPaperImage),
|
|
||||||
const Divider(height: 24),
|
|
||||||
Text("Optional Photos", style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildImageCard("Optional Photo 1", data.optionalImage1, remark: data.optionalRemark1),
|
_buildImageCard("Optional Photo 1", data.optionalImage1, remark: data.optionalRemark1),
|
||||||
_buildImageCard("Optional Photo 2", data.optionalImage2, remark: data.optionalRemark2),
|
_buildImageCard("Optional Photo 2", data.optionalImage2, remark: data.optionalRemark2),
|
||||||
_buildImageCard("Optional Photo 3", data.optionalImage3, remark: data.optionalRemark3),
|
_buildImageCard("Optional Photo 3", data.optionalImage3, remark: data.optionalRemark3),
|
||||||
@ -107,7 +104,6 @@ class RiverInSituStep4Summary extends StatelessWidget {
|
|||||||
_buildParameterListItem(context, icon: Icons.opacity, label: "Turbidity", unit: "NTU", value: data.turbidity?.toStringAsFixed(2)),
|
_buildParameterListItem(context, icon: Icons.opacity, label: "Turbidity", unit: "NTU", value: data.turbidity?.toStringAsFixed(2)),
|
||||||
_buildParameterListItem(context, icon: Icons.filter_alt_outlined, label: "TSS", unit: "mg/L", value: data.tss?.toStringAsFixed(2)),
|
_buildParameterListItem(context, icon: Icons.filter_alt_outlined, label: "TSS", unit: "mg/L", value: data.tss?.toStringAsFixed(2)),
|
||||||
_buildParameterListItem(context, icon: Icons.battery_charging_full, label: "Battery", unit: "V", value: data.batteryVoltage?.toStringAsFixed(2)),
|
_buildParameterListItem(context, icon: Icons.battery_charging_full, label: "Battery", unit: "V", value: data.batteryVoltage?.toStringAsFixed(2)),
|
||||||
// NOTE: If you add river-specific parameters, display them here.
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -155,6 +151,11 @@ class RiverInSituStep4Summary extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDetailRow(String label, String? value) {
|
Widget _buildDetailRow(String label, String? value) {
|
||||||
|
String displayValue = value?.replaceAll('null - null', '').replaceAll('null |', '').replaceAll('| null', '').trim() ?? 'N/A';
|
||||||
|
if (displayValue.isEmpty || displayValue == "-") {
|
||||||
|
displayValue = 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -167,7 +168,7 @@ class RiverInSituStep4Summary extends StatelessWidget {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: Text(value != null && value.isNotEmpty ? value : 'N/A', style: const TextStyle(fontSize: 16)),
|
child: Text(displayValue, style: const TextStyle(fontSize: 16)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -175,7 +176,7 @@ class RiverInSituStep4Summary extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildParameterListItem(BuildContext context, {required IconData icon, required String label, required String unit, required String? value}) {
|
Widget _buildParameterListItem(BuildContext context, {required IconData icon, required String label, required String unit, required String? value}) {
|
||||||
final bool isMissing = value == null;
|
final bool isMissing = value == null || value.contains('-999');
|
||||||
final String displayValue = isMissing ? 'N/A' : '$value ${unit}'.trim();
|
final String displayValue = isMissing ? 'N/A' : '$value ${unit}'.trim();
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
@ -9,7 +9,6 @@ import 'package:path/path.dart' as p;
|
|||||||
|
|
||||||
import '../models/tarball_data.dart';
|
import '../models/tarball_data.dart';
|
||||||
import '../models/in_situ_sampling_data.dart';
|
import '../models/in_situ_sampling_data.dart';
|
||||||
// ADDED: Import the river-specific data model
|
|
||||||
import '../models/river_in_situ_sampling_data.dart';
|
import '../models/river_in_situ_sampling_data.dart';
|
||||||
|
|
||||||
/// A comprehensive service for handling all local data storage for offline submissions.
|
/// A comprehensive service for handling all local data storage for offline submissions.
|
||||||
@ -19,18 +18,15 @@ class LocalStorageService {
|
|||||||
// Part 1: Public Storage Setup
|
// Part 1: Public Storage Setup
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
|
|
||||||
/// Checks for and requests necessary storage permissions for public storage.
|
|
||||||
Future<bool> _requestPermissions() async {
|
Future<bool> _requestPermissions() async {
|
||||||
var status = await Permission.manageExternalStorage.request();
|
var status = await Permission.manageExternalStorage.request();
|
||||||
return status.isGranted;
|
return status.isGranted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the public external storage directory and creates the base MMSV4 folder.
|
|
||||||
Future<Directory?> _getPublicMMSV4Directory() async {
|
Future<Directory?> _getPublicMMSV4Directory() async {
|
||||||
if (await _requestPermissions()) {
|
if (await _requestPermissions()) {
|
||||||
final Directory? externalDir = await getExternalStorageDirectory();
|
final Directory? externalDir = await getExternalStorageDirectory();
|
||||||
if (externalDir != null) {
|
if (externalDir != null) {
|
||||||
// Navigates up from the app-specific folder to the public root
|
|
||||||
final publicRootPath = externalDir.path.split('/Android/')[0];
|
final publicRootPath = externalDir.path.split('/Android/')[0];
|
||||||
final mmsv4Dir = Directory(p.join(publicRootPath, 'MMSV4'));
|
final mmsv4Dir = Directory(p.join(publicRootPath, 'MMSV4'));
|
||||||
if (!await mmsv4Dir.exists()) {
|
if (!await mmsv4Dir.exists()) {
|
||||||
@ -47,7 +43,6 @@ class LocalStorageService {
|
|||||||
// Part 2: Tarball Specific Methods
|
// Part 2: Tarball Specific Methods
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
|
|
||||||
/// Gets the base directory for storing tarball sampling data logs.
|
|
||||||
Future<Directory?> _getTarballBaseDir() async {
|
Future<Directory?> _getTarballBaseDir() async {
|
||||||
final mmsv4Dir = await _getPublicMMSV4Directory();
|
final mmsv4Dir = await _getPublicMMSV4Directory();
|
||||||
if (mmsv4Dir == null) return null;
|
if (mmsv4Dir == null) return null;
|
||||||
@ -59,7 +54,6 @@ class LocalStorageService {
|
|||||||
return tarballDir;
|
return tarballDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Saves a single tarball sampling record to a unique folder in public storage.
|
|
||||||
Future<String?> saveTarballSamplingData(TarballSamplingData data) async {
|
Future<String?> saveTarballSamplingData(TarballSamplingData data) async {
|
||||||
final baseDir = await _getTarballBaseDir();
|
final baseDir = await _getTarballBaseDir();
|
||||||
if (baseDir == null) {
|
if (baseDir == null) {
|
||||||
@ -104,7 +98,6 @@ class LocalStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves all saved tarball submission logs from public storage.
|
|
||||||
Future<List<Map<String, dynamic>>> getAllTarballLogs() async {
|
Future<List<Map<String, dynamic>>> getAllTarballLogs() async {
|
||||||
final baseDir = await _getTarballBaseDir();
|
final baseDir = await _getTarballBaseDir();
|
||||||
if (baseDir == null || !await baseDir.exists()) return [];
|
if (baseDir == null || !await baseDir.exists()) return [];
|
||||||
@ -119,7 +112,7 @@ class LocalStorageService {
|
|||||||
if (await jsonFile.exists()) {
|
if (await jsonFile.exists()) {
|
||||||
final content = await jsonFile.readAsString();
|
final content = await jsonFile.readAsString();
|
||||||
final data = jsonDecode(content) as Map<String, dynamic>;
|
final data = jsonDecode(content) as Map<String, dynamic>;
|
||||||
data['logDirectory'] = entity.path; // Add directory path for resubmission/update
|
data['logDirectory'] = entity.path;
|
||||||
logs.add(data);
|
logs.add(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,7 +124,6 @@ class LocalStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates an existing log file with new submission status.
|
|
||||||
Future<void> updateTarballLog(Map<String, dynamic> updatedLogData) async {
|
Future<void> updateTarballLog(Map<String, dynamic> updatedLogData) async {
|
||||||
final logDir = updatedLogData['logDirectory'];
|
final logDir = updatedLogData['logDirectory'];
|
||||||
if (logDir == null) {
|
if (logDir == null) {
|
||||||
@ -156,7 +148,6 @@ class LocalStorageService {
|
|||||||
// Part 3: Marine In-Situ Specific Methods
|
// Part 3: Marine In-Situ Specific Methods
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
|
|
||||||
/// Gets the base directory for storing marine in-situ sampling data logs.
|
|
||||||
Future<Directory?> _getInSituBaseDir() async {
|
Future<Directory?> _getInSituBaseDir() async {
|
||||||
final mmsv4Dir = await _getPublicMMSV4Directory();
|
final mmsv4Dir = await _getPublicMMSV4Directory();
|
||||||
if (mmsv4Dir == null) return null;
|
if (mmsv4Dir == null) return null;
|
||||||
@ -168,7 +159,6 @@ class LocalStorageService {
|
|||||||
return inSituDir;
|
return inSituDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Saves a single marine in-situ sampling record to a unique folder in public storage.
|
|
||||||
Future<String?> saveInSituSamplingData(InSituSamplingData data) async {
|
Future<String?> saveInSituSamplingData(InSituSamplingData data) async {
|
||||||
final baseDir = await _getInSituBaseDir();
|
final baseDir = await _getInSituBaseDir();
|
||||||
if (baseDir == null) {
|
if (baseDir == null) {
|
||||||
@ -212,7 +202,6 @@ class LocalStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves all saved marine in-situ submission logs from public storage.
|
|
||||||
Future<List<Map<String, dynamic>>> getAllInSituLogs() async {
|
Future<List<Map<String, dynamic>>> getAllInSituLogs() async {
|
||||||
final baseDir = await _getInSituBaseDir();
|
final baseDir = await _getInSituBaseDir();
|
||||||
if (baseDir == null || !await baseDir.exists()) return [];
|
if (baseDir == null || !await baseDir.exists()) return [];
|
||||||
@ -239,7 +228,6 @@ class LocalStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates an existing marine in-situ log file with new submission status.
|
|
||||||
Future<void> updateInSituLog(Map<String, dynamic> updatedLogData) async {
|
Future<void> updateInSituLog(Map<String, dynamic> updatedLogData) async {
|
||||||
final logDir = updatedLogData['logDirectory'];
|
final logDir = updatedLogData['logDirectory'];
|
||||||
if (logDir == null) {
|
if (logDir == null) {
|
||||||
@ -260,15 +248,22 @@ class LocalStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
// ADDED: Part 4: River In-Situ Specific Methods
|
// UPDATED: Part 4: River In-Situ Specific Methods
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
|
|
||||||
/// Gets the base directory for storing river in-situ sampling data logs.
|
/// Gets the base directory for storing river in-situ sampling data logs, organized by sampling type.
|
||||||
Future<Directory?> _getRiverInSituBaseDir() async {
|
Future<Directory?> _getRiverInSituBaseDir(String? samplingType) async {
|
||||||
final mmsv4Dir = await _getPublicMMSV4Directory();
|
final mmsv4Dir = await _getPublicMMSV4Directory();
|
||||||
if (mmsv4Dir == null) return null;
|
if (mmsv4Dir == null) return null;
|
||||||
|
|
||||||
final inSituDir = Directory(p.join(mmsv4Dir.path, 'river', 'river_in_situ_sampling'));
|
String subfolderName;
|
||||||
|
if (samplingType == 'Schedule' || samplingType == 'Triennial') {
|
||||||
|
subfolderName = samplingType!;
|
||||||
|
} else {
|
||||||
|
subfolderName = 'Others';
|
||||||
|
}
|
||||||
|
|
||||||
|
final inSituDir = Directory(p.join(mmsv4Dir.path, 'river', 'river_in_situ_sampling', subfolderName));
|
||||||
if (!await inSituDir.exists()) {
|
if (!await inSituDir.exists()) {
|
||||||
await inSituDir.create(recursive: true);
|
await inSituDir.create(recursive: true);
|
||||||
}
|
}
|
||||||
@ -277,14 +272,15 @@ class LocalStorageService {
|
|||||||
|
|
||||||
/// Saves a single river in-situ sampling record to a unique folder in public storage.
|
/// Saves a single river in-situ sampling record to a unique folder in public storage.
|
||||||
Future<String?> saveRiverInSituSamplingData(RiverInSituSamplingData data) async {
|
Future<String?> saveRiverInSituSamplingData(RiverInSituSamplingData data) async {
|
||||||
final baseDir = await _getRiverInSituBaseDir();
|
// UPDATED: Pass the samplingType to get the correct subdirectory.
|
||||||
|
final baseDir = await _getRiverInSituBaseDir(data.samplingType);
|
||||||
if (baseDir == null) {
|
if (baseDir == null) {
|
||||||
debugPrint("Could not get public storage directory for River In-Situ. Check permissions.");
|
debugPrint("Could not get public storage directory for River In-Situ. Check permissions.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final stationCode = data.selectedStation?['r_man_station_code'] ?? 'UNKNOWN_STATION';
|
final stationCode = data.selectedStation?['sampling_station_code'] ?? 'UNKNOWN_STATION';
|
||||||
final timestamp = "${data.samplingDate}_${data.samplingTime?.replaceAll(':', '-')}";
|
final timestamp = "${data.samplingDate}_${data.samplingTime?.replaceAll(':', '-')}";
|
||||||
final eventFolderName = "${stationCode}_$timestamp";
|
final eventFolderName = "${stationCode}_$timestamp";
|
||||||
final eventDir = Directory(p.join(baseDir.path, eventFolderName));
|
final eventDir = Directory(p.join(baseDir.path, eventFolderName));
|
||||||
@ -319,26 +315,36 @@ class LocalStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves all saved river in-situ submission logs from public storage.
|
/// Retrieves all saved river in-situ submission logs from all subfolders.
|
||||||
Future<List<Map<String, dynamic>>> getAllRiverInSituLogs() async {
|
Future<List<Map<String, dynamic>>> getAllRiverInSituLogs() async {
|
||||||
final baseDir = await _getRiverInSituBaseDir();
|
final mmsv4Dir = await _getPublicMMSV4Directory();
|
||||||
if (baseDir == null || !await baseDir.exists()) return [];
|
if (mmsv4Dir == null) return [];
|
||||||
|
|
||||||
|
final topLevelDir = Directory(p.join(mmsv4Dir.path, 'river', 'river_in_situ_sampling'));
|
||||||
|
if (!await topLevelDir.exists()) return [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final List<Map<String, dynamic>> logs = [];
|
final List<Map<String, dynamic>> logs = [];
|
||||||
final entities = baseDir.listSync();
|
// List all subdirectories (e.g., 'Schedule', 'Triennial', 'Others')
|
||||||
|
final typeSubfolders = topLevelDir.listSync();
|
||||||
|
|
||||||
for (var entity in entities) {
|
for (var typeSubfolder in typeSubfolders) {
|
||||||
if (entity is Directory) {
|
if (typeSubfolder is Directory) {
|
||||||
final jsonFile = File(p.join(entity.path, 'data.json'));
|
// List all event directories inside the type subfolder
|
||||||
|
final eventFolders = typeSubfolder.listSync();
|
||||||
|
for (var eventFolder in eventFolders) {
|
||||||
|
if (eventFolder is Directory) {
|
||||||
|
final jsonFile = File(p.join(eventFolder.path, 'data.json'));
|
||||||
if (await jsonFile.exists()) {
|
if (await jsonFile.exists()) {
|
||||||
final content = await jsonFile.readAsString();
|
final content = await jsonFile.readAsString();
|
||||||
final data = jsonDecode(content) as Map<String, dynamic>;
|
final data = jsonDecode(content) as Map<String, dynamic>;
|
||||||
data['logDirectory'] = entity.path;
|
data['logDirectory'] = eventFolder.path;
|
||||||
logs.add(data);
|
logs.add(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return logs;
|
return logs;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error getting all river in-situ logs: $e");
|
debugPrint("Error getting all river in-situ logs: $e");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user