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 ---
|
||||
String? weather;
|
||||
// CHANGED: Renamed for river context
|
||||
String? waterLevel;
|
||||
String? riverCondition;
|
||||
String? eventRemarks;
|
||||
String? labRemarks;
|
||||
|
||||
// CHANGED: Image descriptions adapted for river context
|
||||
File? leftBankViewImage;
|
||||
File? rightBankViewImage;
|
||||
File? waterFillingImage;
|
||||
File? waterColorImage;
|
||||
File? phPaperImage;
|
||||
File? backgroundStationImage;
|
||||
File? upstreamRiverImage;
|
||||
File? downstreamRiverImage;
|
||||
|
||||
// --- Step 4: Additional Photos ---
|
||||
File? sampleTurbidityImage;
|
||||
|
||||
File? optionalImage1;
|
||||
String? optionalRemark1;
|
||||
@ -64,12 +61,6 @@ class RiverInSituSamplingData {
|
||||
double? tss;
|
||||
double? batteryVoltage;
|
||||
|
||||
// --- START: Add your river-specific parameters here ---
|
||||
// Example:
|
||||
// double? flowRate;
|
||||
// --- END: Add your river-specific parameters here ---
|
||||
|
||||
|
||||
// --- Post-Submission Status ---
|
||||
String? submissionStatus;
|
||||
String? submissionMessage;
|
||||
@ -80,6 +71,69 @@ class RiverInSituSamplingData {
|
||||
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.
|
||||
Map<String, String> toApiFormData() {
|
||||
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
|
||||
add('first_sampler_user_id', firstSamplerUserId);
|
||||
add('r_man_second_sampler_id', secondSampler?['user_id']);
|
||||
@ -108,10 +159,10 @@ class RiverInSituSamplingData {
|
||||
|
||||
// Step 2 Data
|
||||
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_lab_remark', labRemarks);
|
||||
|
||||
// Step 4 Data
|
||||
add('r_man_optional_photo_01_remarks', optionalRemark1);
|
||||
add('r_man_optional_photo_02_remarks', optionalRemark2);
|
||||
add('r_man_optional_photo_03_remarks', optionalRemark3);
|
||||
@ -132,29 +183,22 @@ class RiverInSituSamplingData {
|
||||
add('r_man_tss', tss);
|
||||
add('r_man_battery_volt', batteryVoltage);
|
||||
|
||||
// --- START: Add your new river parameters to the form data map ---
|
||||
// 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
|
||||
// Additional data for display or logging
|
||||
add('first_sampler_name', firstSamplerName);
|
||||
// Assuming river station keys are prefixed with 'r_man_'
|
||||
add('r_man_station_code', selectedStation?['r_man_station_code']);
|
||||
add('r_man_station_name', selectedStation?['r_man_station_name']);
|
||||
add('r_man_station_code', selectedStation?['sampling_station_code']);
|
||||
add('r_man_station_name', selectedStation?['sampling_river']);
|
||||
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/// Converts the image properties into a Map<String, File?> for the multipart API request.
|
||||
Map<String, File?> toApiImageFiles() {
|
||||
// IMPORTANT: Keys adapted for river context.
|
||||
return {
|
||||
'r_man_left_bank_view': leftBankViewImage,
|
||||
'r_man_right_bank_view': rightBankViewImage,
|
||||
'r_man_filling_water_into_sample_bottle': waterFillingImage,
|
||||
'r_man_water_in_clear_glass_bottle': waterColorImage,
|
||||
'r_man_examine_preservative_ph_paper': phPaperImage,
|
||||
'r_man_background_station': backgroundStationImage,
|
||||
'r_man_upstream_river': upstreamRiverImage,
|
||||
'r_man_downstream_river': downstreamRiverImage,
|
||||
'r_man_sample_turbidity': sampleTurbidityImage,
|
||||
'r_man_optional_photo_01': optionalImage1,
|
||||
'r_man_optional_photo_02': optionalImage2,
|
||||
'r_man_optional_photo_03': optionalImage3,
|
||||
|
||||
@ -1,17 +1,13 @@
|
||||
// lib/screens/river/manual/widgets/data_status_log.dart
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
// CHANGED: Import River-specific models and services
|
||||
import '../../../../models/river_in_situ_sampling_data.dart';
|
||||
import '../../../../services/local_storage_service.dart';
|
||||
import '../../../../services/river_api_service.dart';
|
||||
|
||||
// A unified model to represent any type of submission log entry.
|
||||
class SubmissionLogEntry {
|
||||
final String type; // e.g., 'in-situ'
|
||||
final String type;
|
||||
final String title;
|
||||
final String stationCode;
|
||||
final DateTime submissionDateTime;
|
||||
@ -34,169 +30,134 @@ class SubmissionLogEntry {
|
||||
});
|
||||
}
|
||||
|
||||
// CHANGED: Renamed widget for River context
|
||||
class RiverDataStatusLog extends StatefulWidget {
|
||||
const RiverDataStatusLog({super.key});
|
||||
|
||||
@override
|
||||
// CHANGED: Renamed state class
|
||||
State<RiverDataStatusLog> createState() => _RiverDataStatusLogState();
|
||||
}
|
||||
|
||||
// CHANGED: Renamed state class
|
||||
class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
||||
final LocalStorageService _localStorageService = LocalStorageService();
|
||||
// CHANGED: Use RiverApiService
|
||||
final RiverApiService _riverApiService = RiverApiService();
|
||||
|
||||
Map<String, List<SubmissionLogEntry>> _groupedLogs = {};
|
||||
Map<String, List<SubmissionLogEntry>> _filteredLogs = {};
|
||||
final Map<String, bool> _isCategoryExpanded = {};
|
||||
// Raw data lists
|
||||
List<SubmissionLogEntry> _scheduleLogs = [];
|
||||
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;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadAllLogs();
|
||||
_searchController.addListener(_filterLogs);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
for (var controller in _searchControllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Loads logs for the river in-situ module.
|
||||
Future<void> _loadAllLogs() async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
// NOTE: Assumes a method exists in your local storage service for river logs.
|
||||
final inSituLogs = await _localStorageService.getAllRiverInSituLogs();
|
||||
final riverLogs = 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.
|
||||
|
||||
// Map In-Situ logs for River
|
||||
final List<SubmissionLogEntry> inSituEntries = [];
|
||||
for (var log in inSituLogs) {
|
||||
final String dateStr = log['data_capture_date'] ?? log['sampling_date'] ?? '';
|
||||
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(),
|
||||
for (var log in riverLogs) {
|
||||
final entry = SubmissionLogEntry(
|
||||
type: log['r_man_type'] as String? ?? 'Others',
|
||||
title: log['selectedStation']?['sampling_river'] ?? 'Unknown Station',
|
||||
stationCode: log['selectedStation']?['sampling_station_code'] ?? 'N/A',
|
||||
submissionDateTime: DateTime.tryParse('${log['r_man_date']} ${log['r_man_time']}') ?? DateTime.now(),
|
||||
reportId: log['reportId']?.toString(),
|
||||
status: log['submissionStatus'] ?? 'L1',
|
||||
message: log['submissionMessage'] ?? 'No status message.',
|
||||
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) {
|
||||
setState(() {
|
||||
_groupedLogs = tempGroupedLogs;
|
||||
_filteredLogs = tempGroupedLogs;
|
||||
_scheduleLogs = tempSchedule;
|
||||
_triennialLogs = tempTriennial;
|
||||
_otherLogs = tempOthers;
|
||||
_isLoading = false;
|
||||
});
|
||||
_filterLogs(); // Perform initial filter
|
||||
}
|
||||
}
|
||||
|
||||
void _filterLogs() {
|
||||
final query = _searchController.text.toLowerCase();
|
||||
final Map<String, List<SubmissionLogEntry>> tempFiltered = {};
|
||||
final scheduleQuery = _searchControllers['Schedule']?.text.toLowerCase() ?? '';
|
||||
final triennialQuery = _searchControllers['Triennial']?.text.toLowerCase() ?? '';
|
||||
final otherQuery = _searchControllers['Others']?.text.toLowerCase() ?? '';
|
||||
|
||||
_groupedLogs.forEach((category, logs) {
|
||||
final filtered = logs.where((log) {
|
||||
setState(() {
|
||||
_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) ||
|
||||
log.stationCode.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 {
|
||||
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;
|
||||
|
||||
// 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 dataToResubmit = RiverInSituSamplingData.fromJson(logData);
|
||||
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];
|
||||
if (imagePath is String && imagePath.isNotEmpty) {
|
||||
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(
|
||||
formData: dataToResubmit.toApiFormData(),
|
||||
imageFiles: imageFiles,
|
||||
);
|
||||
|
||||
logData['submissionStatus'] = result['status'];
|
||||
logData['submissionMessage'] = result['message'];
|
||||
logData['reportId'] = result['reportId']?.toString() ?? logData['reportId'];
|
||||
|
||||
// NOTE: Assumes a method exists to update river logs.
|
||||
await _localStorageService.updateRiverInSituLog(logData);
|
||||
|
||||
if (mounted) await _loadAllLogs();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasAnyLogs = _scheduleLogs.isNotEmpty || _triennialLogs.isNotEmpty || _otherLogs.isNotEmpty;
|
||||
final hasFilteredLogs = _filteredScheduleLogs.isNotEmpty || _filteredTriennialLogs.isNotEmpty || _filteredOtherLogs.isNotEmpty;
|
||||
|
||||
return Scaffold(
|
||||
// CHANGED: Updated AppBar title
|
||||
appBar: AppBar(title: const Text('River Data Status Log')),
|
||||
body: Column(
|
||||
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
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: RefreshIndicator(
|
||||
onRefresh: _loadAllLogs,
|
||||
child: _filteredLogs.isEmpty
|
||||
? Center(child: Text(_groupedLogs.isEmpty ? 'No submission logs found.' : 'No logs match your search.'))
|
||||
child: !hasAnyLogs
|
||||
? const Center(child: Text('No submission logs found.'))
|
||||
: ListView(
|
||||
children: _filteredLogs.entries.map((entry) {
|
||||
return _buildCategorySection(entry.key, entry.value);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
children: [
|
||||
// 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) {
|
||||
final bool isExpanded = _isCategoryExpanded[category] ?? false;
|
||||
final int itemCount = isExpanded ? logs.length : (logs.length > 5 ? 5 : logs.length);
|
||||
// Calculate the height for the scrollable list.
|
||||
// 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(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
|
||||
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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(),
|
||||
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,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: itemCount,
|
||||
itemCount: logs.length,
|
||||
itemBuilder: (context, 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),
|
||||
trailing: isFailed
|
||||
? (log.isResubmitting
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 3),
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.sync, color: Colors.blue),
|
||||
tooltip: 'Resubmit',
|
||||
onPressed: () => _resubmitData(log),
|
||||
))
|
||||
? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 3))
|
||||
: IconButton(icon: const Icon(Icons.sync, color: Colors.blue), tooltip: 'Resubmit', onPressed: () => _resubmitData(log)))
|
||||
: null,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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_2_site_info.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 {
|
||||
const RiverInSituSamplingScreen({super.key});
|
||||
|
||||
@ -27,10 +26,7 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
||||
|
||||
late RiverInSituSamplingData _data;
|
||||
|
||||
// A single instance of the service to be used by all child widgets.
|
||||
final RiverInSituSamplingService _samplingService = RiverInSituSamplingService();
|
||||
|
||||
// Service for saving submission logs locally.
|
||||
final LocalStorageService _localStorageService = LocalStorageService();
|
||||
|
||||
int _currentPage = 0;
|
||||
@ -39,8 +35,6 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Creates a NEW data object with the CURRENT date and time
|
||||
// every time the user starts a new sampling.
|
||||
_data = RiverInSituSamplingData(
|
||||
samplingDate: DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
||||
samplingTime: DateFormat('HH:mm:ss').format(DateTime.now()),
|
||||
@ -54,9 +48,8 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Navigates to the next page in the form.
|
||||
void _nextPage() {
|
||||
if (_currentPage < 3) {
|
||||
if (_currentPage < 4) {
|
||||
_pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
@ -64,7 +57,6 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigates to the previous page in the form.
|
||||
void _previousPage() {
|
||||
if (_currentPage > 0) {
|
||||
_pageController.previousPage(
|
||||
@ -74,7 +66,6 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the final submission process.
|
||||
Future<void> _submitForm() async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
@ -86,7 +77,6 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
||||
_data.submissionMessage = result['message'];
|
||||
_data.reportId = result['reportId']?.toString();
|
||||
|
||||
// Save a log of the submission locally using the river-specific method.
|
||||
await _localStorageService.saveRiverInSituSamplingData(_data);
|
||||
|
||||
setState(() => _isLoading = false);
|
||||
@ -100,19 +90,18 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Use Provider.value to provide the existing river service instance to all child widgets.
|
||||
return Provider.value(
|
||||
value: _samplingService,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('In-Situ Sampling (${_currentPage + 1}/4)'),
|
||||
title: Text('In-Situ Sampling (${_currentPage + 1}/5)'),
|
||||
leading: _currentPage > 0
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
@ -129,11 +118,11 @@ class _RiverInSituSamplingScreenState extends State<RiverInSituSamplingScreen> {
|
||||
});
|
||||
},
|
||||
children: [
|
||||
// Each step is a separate river-specific widget.
|
||||
RiverInSituStep1SamplingInfo(data: _data, onNext: _nextPage),
|
||||
RiverInSituStep2SiteInfo(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 _currentLatController;
|
||||
late final TextEditingController _currentLonController;
|
||||
// REMOVED: Controllers for weather and remarks.
|
||||
|
||||
List<String> _statesList = [];
|
||||
List<Map<String, dynamic>> _stationsForState = [];
|
||||
final List<String> _samplingTypes = ['Schedule', 'Ad-Hoc', 'Complaint'];
|
||||
final List<String> _samplingTypes = ['Schedule', 'Triennial'];
|
||||
// REMOVED: Weather options list.
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -58,6 +60,7 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
||||
_stationLonController.dispose();
|
||||
_currentLatController.dispose();
|
||||
_currentLonController.dispose();
|
||||
// REMOVED: Dispose controllers for remarks.
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -70,6 +73,7 @@ class _RiverInSituStep1SamplingInfoState extends State<RiverInSituStep1SamplingI
|
||||
_stationLonController = TextEditingController(text: widget.data.stationLongitude);
|
||||
_currentLatController = TextEditingController(text: widget.data.currentLatitude);
|
||||
_currentLonController = TextEditingController(text: widget.data.currentLongitude);
|
||||
// REMOVED: Initialize controllers for remarks.
|
||||
}
|
||||
|
||||
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),
|
||||
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(
|
||||
onPressed: _goToNextStep,
|
||||
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 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
@ -26,33 +28,19 @@ class _RiverInSituStep2SiteInfoState extends State<RiverInSituStep2SiteInfo> {
|
||||
|
||||
late final TextEditingController _eventRemarksController;
|
||||
late final TextEditingController _labRemarksController;
|
||||
late final TextEditingController _optionalRemark1Controller;
|
||||
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
|
||||
final List<String> _weatherOptions = ['Clear', 'Rainy', 'Cloudy'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_eventRemarksController = TextEditingController(text: widget.data.eventRemarks);
|
||||
_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
|
||||
void dispose() {
|
||||
_eventRemarksController.dispose();
|
||||
_labRemarksController.dispose();
|
||||
_optionalRemark1Controller.dispose();
|
||||
_optionalRemark2Controller.dispose();
|
||||
_optionalRemark3Controller.dispose();
|
||||
_optionalRemark4Controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -79,20 +67,16 @@ class _RiverInSituStep2SiteInfoState extends State<RiverInSituStep2SiteInfo> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.data.leftBankViewImage == null ||
|
||||
widget.data.rightBankViewImage == null ||
|
||||
widget.data.waterFillingImage == null ||
|
||||
widget.data.waterColorImage == null ||
|
||||
widget.data.phPaperImage == null) {
|
||||
_showSnackBar('Please attach all 5 required photos before proceeding.', isError: true);
|
||||
_formKey.currentState!.save();
|
||||
|
||||
// UPDATED: Validation now checks for 3 required photos.
|
||||
if (widget.data.backgroundStationImage == null ||
|
||||
widget.data.upstreamRiverImage == null ||
|
||||
widget.data.downstreamRiverImage == null) {
|
||||
_showSnackBar('Please attach all 3 required photos before proceeding.', isError: true);
|
||||
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();
|
||||
}
|
||||
|
||||
@ -112,39 +96,17 @@ class _RiverInSituStep2SiteInfoState extends State<RiverInSituStep2SiteInfo> {
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
children: [
|
||||
Text("On-Site Information", style: Theme.of(context).textTheme.headlineSmall),
|
||||
const SizedBox(height: 24),
|
||||
Text("On-Site Information", style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
value: widget.data.weather,
|
||||
items: _weatherOptions.map((item) => DropdownMenuItem(value: item, child: Text(item))).toList(),
|
||||
onChanged: (value) => setState(() => widget.data.weather = value),
|
||||
decoration: const InputDecoration(labelText: 'Weather *'),
|
||||
validator: (value) => value == null ? 'Weather is required' : null,
|
||||
onSaved: (value) => widget.data.weather = value,
|
||||
),
|
||||
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(
|
||||
controller: _eventRemarksController,
|
||||
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,
|
||||
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),
|
||||
ElevatedButton(
|
||||
onPressed: _goToNextStep,
|
||||
|
||||
@ -8,11 +8,12 @@ import 'package:usb_serial/usb_serial.dart';
|
||||
|
||||
import '../../../../models/river_in_situ_sampling_data.dart';
|
||||
import '../../../../services/river_in_situ_sampling_service.dart';
|
||||
import '../../../../bluetooth/bluetooth_manager.dart'; // For connection state enum
|
||||
import '../../../../serial/serial_manager.dart'; // For connection state enum
|
||||
import '../../../../bluetooth/bluetooth_manager.dart';
|
||||
import '../../../../serial/serial_manager.dart';
|
||||
import '../../../../bluetooth/widgets/bluetooth_device_list_dialog.dart';
|
||||
import '../../../../serial/widget/serial_port_list_dialog.dart';
|
||||
|
||||
// UPDATED: Class name changed from RiverInSituStep2DataCapture to RiverInSituStep3DataCapture
|
||||
class RiverInSituStep3DataCapture extends StatefulWidget {
|
||||
final RiverInSituSamplingData data;
|
||||
final VoidCallback onNext;
|
||||
@ -24,9 +25,11 @@ class RiverInSituStep3DataCapture extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
// UPDATED: State class reference
|
||||
State<RiverInSituStep3DataCapture> createState() => _RiverInSituStep3DataCaptureState();
|
||||
}
|
||||
|
||||
// UPDATED: State class name
|
||||
class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCapture> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = false;
|
||||
@ -48,7 +51,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
final _turbidityController = TextEditingController();
|
||||
final _tssController = TextEditingController();
|
||||
final _batteryController = TextEditingController();
|
||||
// NOTE: If you add river-specific parameters, add their controllers here.
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -81,7 +83,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
widget.data.turbidity ??= -999.0;
|
||||
widget.data.tss ??= -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();
|
||||
_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.filter_alt_outlined, 'label': 'TSS', 'unit': 'mg/L', 'controller': _tssController},
|
||||
{'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();
|
||||
_tssController.dispose();
|
||||
_batteryController.dispose();
|
||||
// NOTE: Dispose your river-specific controllers here.
|
||||
}
|
||||
|
||||
Future<void> _handleConnectionAttempt(String type) async {
|
||||
final service = context.read<RiverInSituSamplingService>();
|
||||
|
||||
final bool hasPermissions = await service.requestDevicePermissions();
|
||||
if (!hasPermissions && mounted) {
|
||||
_showSnackBar("Bluetooth & Location permissions are required to connect.", isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
_disconnectFromAll();
|
||||
await Future.delayed(const Duration(milliseconds: 250));
|
||||
|
||||
final bool connectionSuccess = await _connectToDevice(type);
|
||||
|
||||
if (connectionSuccess && mounted) {
|
||||
_dataSubscription?.cancel();
|
||||
final stream = type == 'bluetooth' ? service.bluetoothDataStream : service.serialDataStream;
|
||||
|
||||
_dataSubscription = stream.listen((readings) {
|
||||
if (mounted) {
|
||||
_updateTextFields(readings);
|
||||
@ -158,7 +152,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
setState(() => _isLoading = true);
|
||||
final service = context.read<RiverInSituSamplingService>();
|
||||
bool success = false;
|
||||
|
||||
try {
|
||||
if (type == 'bluetooth') {
|
||||
final devices = await service.getPairedBluetoothDevices();
|
||||
@ -255,22 +248,14 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
title: const Text('Data Collection Active'),
|
||||
content: const Text('Please stop the live data collection before proceeding.'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('OK'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
TextButton(child: const Text('OK'), onPressed: () => Navigator.of(context).pop())
|
||||
]);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (_formKey.currentState!.validate()){
|
||||
_formKey.currentState!.save();
|
||||
|
||||
try {
|
||||
const defaultValue = -999.0;
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
widget.onNext();
|
||||
}
|
||||
}
|
||||
@ -321,7 +305,6 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
// CORRECTED: Scrolling is enabled by removing the physics property.
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
children: [
|
||||
Text("Data Capture", style: Theme.of(context).textTheme.headlineSmall),
|
||||
@ -370,10 +353,14 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
valueListenable: service.sondeId,
|
||||
builder: (context, sondeId, child) {
|
||||
final newSondeId = sondeId ?? '';
|
||||
if (_sondeIdController.text != newSondeId) {
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _sondeIdController.text != newSondeId) {
|
||||
_sondeIdController.text = newSondeId;
|
||||
widget.data.sondeId = newSondeId;
|
||||
}
|
||||
});
|
||||
|
||||
return TextFormField(
|
||||
controller: _sondeIdController,
|
||||
decoration: const InputDecoration(
|
||||
@ -447,14 +434,12 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
||||
Widget _buildConnectionCard({required String type, required dynamic connectionState, String? deviceName}) {
|
||||
final isConnected = connectionState == BluetoothConnectionState.connected || connectionState == SerialConnectionState.connected;
|
||||
final isConnecting = connectionState == BluetoothConnectionState.connecting || connectionState == SerialConnectionState.connecting;
|
||||
|
||||
Color statusColor = isConnected ? Colors.green : Colors.red;
|
||||
String statusText = isConnected ? 'Connected to ${deviceName ?? 'device'}' : 'Disconnected';
|
||||
if (isConnecting) {
|
||||
statusColor = Colors.orange;
|
||||
statusText = 'Connecting...';
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
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 'package:flutter/material.dart';
|
||||
|
||||
// CHANGED: Import river-specific data model
|
||||
import '../../../../models/river_in_situ_sampling_data.dart';
|
||||
|
||||
// CHANGED: Renamed class for river context
|
||||
class RiverInSituStep4Summary extends StatelessWidget {
|
||||
// CHANGED: Expects river-specific data model
|
||||
class RiverInSituStep5Summary extends StatelessWidget {
|
||||
final RiverInSituSamplingData data;
|
||||
final VoidCallback onSubmit;
|
||||
final bool isLoading;
|
||||
|
||||
const RiverInSituStep4Summary({
|
||||
const RiverInSituStep5Summary({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.onSubmit,
|
||||
@ -44,45 +41,45 @@ class RiverInSituStep4Summary extends StatelessWidget {
|
||||
_buildDetailRow("Sample ID Code:", data.sampleIdCode),
|
||||
const Divider(height: 20),
|
||||
_buildDetailRow("State:", data.selectedStateName),
|
||||
_buildDetailRow("Category:", data.selectedCategoryName),
|
||||
// CHANGED: Use river-specific station keys
|
||||
_buildDetailRow("Station Code:", data.selectedStation?['r_man_station_code']?.toString()),
|
||||
_buildDetailRow("Station Name:", data.selectedStation?['r_man_station_name']?.toString()),
|
||||
_buildDetailRow(
|
||||
"Station:",
|
||||
"${data.selectedStation?['sampling_station_code']} | ${data.selectedStation?['sampling_river']} | ${data.selectedStation?['sampling_basin']}"
|
||||
),
|
||||
_buildDetailRow("Station Location:", "${data.stationLatitude}, ${data.stationLongitude}"),
|
||||
// REMOVED: Weather and remarks moved to the next section.
|
||||
],
|
||||
),
|
||||
|
||||
_buildSectionCard(
|
||||
context,
|
||||
"Location & On-Site Info",
|
||||
"Site Info & Required Photos",
|
||||
[
|
||||
_buildDetailRow("Current Location:", "${data.currentLatitude}, ${data.currentLongitude}"),
|
||||
_buildDetailRow("Distance Difference:", data.distanceDifferenceInKm != null ? "${(data.distanceDifferenceInKm! * 1000).toStringAsFixed(0)} meters" : "N/A"),
|
||||
if (data.distanceDifferenceRemarks != null && data.distanceDifferenceRemarks!.isNotEmpty)
|
||||
_buildDetailRow("Distance Remarks:", data.distanceDifferenceRemarks),
|
||||
const Divider(height: 20),
|
||||
|
||||
// ADDED: Display for Weather and Remarks.
|
||||
_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("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(
|
||||
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 2", data.optionalImage2, remark: data.optionalRemark2),
|
||||
_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.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)),
|
||||
// NOTE: If you add river-specific parameters, display them here.
|
||||
],
|
||||
),
|
||||
|
||||
@ -155,6 +151,11 @@ class RiverInSituStep4Summary extends StatelessWidget {
|
||||
}
|
||||
|
||||
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(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
child: Row(
|
||||
@ -167,7 +168,7 @@ class RiverInSituStep4Summary extends StatelessWidget {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(value != null && value.isNotEmpty ? value : 'N/A', style: const TextStyle(fontSize: 16)),
|
||||
child: Text(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}) {
|
||||
final bool isMissing = value == null;
|
||||
final bool isMissing = value == null || value.contains('-999');
|
||||
final String displayValue = isMissing ? 'N/A' : '$value ${unit}'.trim();
|
||||
|
||||
return ListTile(
|
||||
@ -9,7 +9,6 @@ import 'package:path/path.dart' as p;
|
||||
|
||||
import '../models/tarball_data.dart';
|
||||
import '../models/in_situ_sampling_data.dart';
|
||||
// ADDED: Import the river-specific data model
|
||||
import '../models/river_in_situ_sampling_data.dart';
|
||||
|
||||
/// A comprehensive service for handling all local data storage for offline submissions.
|
||||
@ -19,18 +18,15 @@ class LocalStorageService {
|
||||
// Part 1: Public Storage Setup
|
||||
// =======================================================================
|
||||
|
||||
/// Checks for and requests necessary storage permissions for public storage.
|
||||
Future<bool> _requestPermissions() async {
|
||||
var status = await Permission.manageExternalStorage.request();
|
||||
return status.isGranted;
|
||||
}
|
||||
|
||||
/// Gets the public external storage directory and creates the base MMSV4 folder.
|
||||
Future<Directory?> _getPublicMMSV4Directory() async {
|
||||
if (await _requestPermissions()) {
|
||||
final Directory? externalDir = await getExternalStorageDirectory();
|
||||
if (externalDir != null) {
|
||||
// Navigates up from the app-specific folder to the public root
|
||||
final publicRootPath = externalDir.path.split('/Android/')[0];
|
||||
final mmsv4Dir = Directory(p.join(publicRootPath, 'MMSV4'));
|
||||
if (!await mmsv4Dir.exists()) {
|
||||
@ -47,7 +43,6 @@ class LocalStorageService {
|
||||
// Part 2: Tarball Specific Methods
|
||||
// =======================================================================
|
||||
|
||||
/// Gets the base directory for storing tarball sampling data logs.
|
||||
Future<Directory?> _getTarballBaseDir() async {
|
||||
final mmsv4Dir = await _getPublicMMSV4Directory();
|
||||
if (mmsv4Dir == null) return null;
|
||||
@ -59,7 +54,6 @@ class LocalStorageService {
|
||||
return tarballDir;
|
||||
}
|
||||
|
||||
/// Saves a single tarball sampling record to a unique folder in public storage.
|
||||
Future<String?> saveTarballSamplingData(TarballSamplingData data) async {
|
||||
final baseDir = await _getTarballBaseDir();
|
||||
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 {
|
||||
final baseDir = await _getTarballBaseDir();
|
||||
if (baseDir == null || !await baseDir.exists()) return [];
|
||||
@ -119,7 +112,7 @@ class LocalStorageService {
|
||||
if (await jsonFile.exists()) {
|
||||
final content = await jsonFile.readAsString();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -131,7 +124,6 @@ class LocalStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates an existing log file with new submission status.
|
||||
Future<void> updateTarballLog(Map<String, dynamic> updatedLogData) async {
|
||||
final logDir = updatedLogData['logDirectory'];
|
||||
if (logDir == null) {
|
||||
@ -156,7 +148,6 @@ class LocalStorageService {
|
||||
// Part 3: Marine In-Situ Specific Methods
|
||||
// =======================================================================
|
||||
|
||||
/// Gets the base directory for storing marine in-situ sampling data logs.
|
||||
Future<Directory?> _getInSituBaseDir() async {
|
||||
final mmsv4Dir = await _getPublicMMSV4Directory();
|
||||
if (mmsv4Dir == null) return null;
|
||||
@ -168,7 +159,6 @@ class LocalStorageService {
|
||||
return inSituDir;
|
||||
}
|
||||
|
||||
/// Saves a single marine in-situ sampling record to a unique folder in public storage.
|
||||
Future<String?> saveInSituSamplingData(InSituSamplingData data) async {
|
||||
final baseDir = await _getInSituBaseDir();
|
||||
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 {
|
||||
final baseDir = await _getInSituBaseDir();
|
||||
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 {
|
||||
final logDir = updatedLogData['logDirectory'];
|
||||
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.
|
||||
Future<Directory?> _getRiverInSituBaseDir() async {
|
||||
/// Gets the base directory for storing river in-situ sampling data logs, organized by sampling type.
|
||||
Future<Directory?> _getRiverInSituBaseDir(String? samplingType) async {
|
||||
final mmsv4Dir = await _getPublicMMSV4Directory();
|
||||
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()) {
|
||||
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.
|
||||
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) {
|
||||
debugPrint("Could not get public storage directory for River In-Situ. Check permissions.");
|
||||
return null;
|
||||
}
|
||||
|
||||
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 eventFolderName = "${stationCode}_$timestamp";
|
||||
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 {
|
||||
final baseDir = await _getRiverInSituBaseDir();
|
||||
if (baseDir == null || !await baseDir.exists()) return [];
|
||||
final mmsv4Dir = await _getPublicMMSV4Directory();
|
||||
if (mmsv4Dir == null) return [];
|
||||
|
||||
final topLevelDir = Directory(p.join(mmsv4Dir.path, 'river', 'river_in_situ_sampling'));
|
||||
if (!await topLevelDir.exists()) return [];
|
||||
|
||||
try {
|
||||
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) {
|
||||
if (entity is Directory) {
|
||||
final jsonFile = File(p.join(entity.path, 'data.json'));
|
||||
for (var typeSubfolder in typeSubfolders) {
|
||||
if (typeSubfolder is Directory) {
|
||||
// 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()) {
|
||||
final content = await jsonFile.readAsString();
|
||||
final data = jsonDecode(content) as Map<String, dynamic>;
|
||||
data['logDirectory'] = entity.path;
|
||||
data['logDirectory'] = eventFolder.path;
|
||||
logs.add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return logs;
|
||||
} catch (e) {
|
||||
debugPrint("Error getting all river in-situ logs: $e");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user