fix air manual data status log
This commit is contained in:
parent
f7fefd3f45
commit
a2d8b372e6
@ -1,4 +1,3 @@
|
||||
// lib/models/air_collection_data.dart
|
||||
import 'dart:io';
|
||||
|
||||
class AirCollectionData {
|
||||
@ -189,7 +188,6 @@ class AirCollectionData {
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'air_man_id': airManId?.toString(),
|
||||
// **THE FIX**: Add the missing user ID to the JSON payload.
|
||||
'air_man_collection_user_id': collectionUserId?.toString(),
|
||||
'air_man_collection_date': collectionDate,
|
||||
'air_man_collection_time': collectionTime,
|
||||
|
||||
@ -91,9 +91,17 @@ class RiverInSituSamplingData {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ADDED HELPER FUNCTION TO FIX THE ERROR
|
||||
int? intFromJson(dynamic value) {
|
||||
if (value is int) return value;
|
||||
if (value is String) return int.tryParse(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
return RiverInSituSamplingData()
|
||||
..firstSamplerName = json['first_sampler_name']
|
||||
..firstSamplerUserId = json['first_sampler_user_id']
|
||||
// MODIFIED THIS LINE TO USE THE HELPER FUNCTION
|
||||
..firstSamplerUserId = intFromJson(json['first_sampler_user_id'])
|
||||
..secondSampler = json['secondSampler']
|
||||
..samplingDate = json['r_man_date']
|
||||
..samplingTime = json['r_man_time']
|
||||
|
||||
@ -1,32 +1,429 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class AirManualDataStatusLog extends StatelessWidget {
|
||||
final List<Map<String, String>> logEntries = [
|
||||
{"Date": "2025-07-15", "Status": "Submitted", "User": "analyst_air"},
|
||||
{"Date": "2025-07-16", "Status": "Approved", "User": "supervisor_air"},
|
||||
];
|
||||
import '../../../../models/air_installation_data.dart';
|
||||
import '../../../../models/air_collection_data.dart';
|
||||
import '../../../../services/local_storage_service.dart';
|
||||
import '../../../../services/api_service.dart';
|
||||
|
||||
class SubmissionLogEntry {
|
||||
final String type;
|
||||
final String title;
|
||||
final String stationCode;
|
||||
final DateTime submissionDateTime;
|
||||
final String? reportId;
|
||||
final String status;
|
||||
final String message;
|
||||
final Map<String, dynamic> rawData;
|
||||
bool isResubmitting;
|
||||
|
||||
SubmissionLogEntry({
|
||||
required this.type,
|
||||
required this.title,
|
||||
required this.stationCode,
|
||||
required this.submissionDateTime,
|
||||
this.reportId,
|
||||
required this.status,
|
||||
required this.message,
|
||||
required this.rawData,
|
||||
this.isResubmitting = false,
|
||||
});
|
||||
}
|
||||
|
||||
class AirManualDataStatusLog extends StatefulWidget {
|
||||
const AirManualDataStatusLog({super.key});
|
||||
|
||||
@override
|
||||
State<AirManualDataStatusLog> createState() => _AirManualDataStatusLogState();
|
||||
}
|
||||
|
||||
class _AirManualDataStatusLogState extends State<AirManualDataStatusLog> {
|
||||
final LocalStorageService _localStorageService = LocalStorageService();
|
||||
final ApiService _apiService = ApiService();
|
||||
|
||||
List<SubmissionLogEntry> _installationLogs = [];
|
||||
List<SubmissionLogEntry> _collectionLogs = [];
|
||||
List<SubmissionLogEntry> _filteredInstallationLogs = [];
|
||||
List<SubmissionLogEntry> _filteredCollectionLogs = [];
|
||||
|
||||
final Map<String, TextEditingController> _searchControllers = {};
|
||||
bool _isLoading = true;
|
||||
final Map<String, bool> _isResubmitting = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchControllers['Installation'] = TextEditingController()..addListener(_filterLogs);
|
||||
_searchControllers['Collection'] = TextEditingController()..addListener(_filterLogs);
|
||||
_loadAllLogs();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchControllers['Installation']?.dispose();
|
||||
_searchControllers['Collection']?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadAllLogs() async {
|
||||
setState(() => _isLoading = true);
|
||||
final airLogs = await _localStorageService.getAllAirSamplingLogs();
|
||||
|
||||
final List<SubmissionLogEntry> tempInstallation = [];
|
||||
final List<SubmissionLogEntry> tempCollection = [];
|
||||
|
||||
for (var log in airLogs) {
|
||||
try {
|
||||
// Determine if this is an installation or collection
|
||||
final hasCollectionData = log['collectionData'] != null && (log['collectionData'] as Map).isNotEmpty;
|
||||
final isInstallation = !hasCollectionData && log['air_man_id'] != null;
|
||||
|
||||
// Get station info from the correct location
|
||||
final stationInfo = isInstallation
|
||||
? log['stationInfo'] ?? {}
|
||||
: log['collectionData']?['stationInfo'] ?? {};
|
||||
|
||||
final stationName = stationInfo['station_name'] ?? 'Station ${log['stationID'] ?? 'Unknown'}';
|
||||
final stationCode = stationInfo['station_code'] ?? log['stationID'] ?? 'N/A';
|
||||
|
||||
// Get correct timestamp
|
||||
final submissionDateTime = isInstallation
|
||||
? _parseInstallationDateTime(log)
|
||||
: _parseCollectionDateTime(log['collectionData']);
|
||||
|
||||
debugPrint('Processed ${isInstallation ? 'Installation' : 'Collection'} log:');
|
||||
debugPrint(' Station: $stationName ($stationCode)');
|
||||
debugPrint(' Original Date: ${submissionDateTime.toString()}');
|
||||
|
||||
final entry = SubmissionLogEntry(
|
||||
type: isInstallation ? 'Installation' : 'Collection',
|
||||
title: stationName,
|
||||
stationCode: stationCode,
|
||||
submissionDateTime: submissionDateTime,
|
||||
reportId: isInstallation ? log['air_man_id']?.toString() : log['collectionData']?['air_man_id']?.toString(),
|
||||
status: log['status'] ?? 'L1',
|
||||
message: _getStatusMessage(log),
|
||||
rawData: log,
|
||||
);
|
||||
|
||||
if (isInstallation) {
|
||||
tempInstallation.add(entry);
|
||||
} else {
|
||||
tempCollection.add(entry);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error processing log entry: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by datetime descending
|
||||
tempInstallation.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
|
||||
tempCollection.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_installationLogs = tempInstallation;
|
||||
_collectionLogs = tempCollection;
|
||||
_isLoading = false;
|
||||
});
|
||||
_filterLogs();
|
||||
}
|
||||
}
|
||||
|
||||
DateTime _parseInstallationDateTime(Map<String, dynamic> log) {
|
||||
try {
|
||||
// First try to get the installation date and time
|
||||
if (log['installationDate'] != null) {
|
||||
final date = log['installationDate'];
|
||||
final time = log['installationTime'] ?? '00:00';
|
||||
return DateFormat('yyyy-MM-dd HH:mm').parse('$date $time');
|
||||
}
|
||||
|
||||
// Fallback to sampling date if installation date not available
|
||||
if (log['samplingDate'] != null) {
|
||||
final date = log['samplingDate'];
|
||||
final time = log['samplingTime'] ?? '00:00';
|
||||
return DateFormat('yyyy-MM-dd HH:mm').parse('$date $time');
|
||||
}
|
||||
|
||||
// If no dates found, check in the raw data structure
|
||||
if (log['data'] != null && log['data']['installationDate'] != null) {
|
||||
final date = log['data']['installationDate'];
|
||||
final time = log['data']['installationTime'] ?? '00:00';
|
||||
return DateFormat('yyyy-MM-dd HH:mm').parse('$date $time');
|
||||
}
|
||||
|
||||
debugPrint('No valid installation date found in log: ${log.keys}');
|
||||
return DateTime.now();
|
||||
} catch (e) {
|
||||
debugPrint('Error parsing installation date: $e');
|
||||
return DateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
DateTime _parseCollectionDateTime(Map<String, dynamic>? collectionData) {
|
||||
try {
|
||||
if (collectionData == null) {
|
||||
debugPrint('Collection data is null');
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
// First try the direct fields
|
||||
if (collectionData['collectionDate'] != null) {
|
||||
final date = collectionData['collectionDate'];
|
||||
final time = collectionData['collectionTime'] ?? '00:00';
|
||||
return DateFormat('yyyy-MM-dd HH:mm').parse('$date $time');
|
||||
}
|
||||
|
||||
// Check for nested data structure
|
||||
if (collectionData['data'] != null && collectionData['data']['collectionDate'] != null) {
|
||||
final date = collectionData['data']['collectionDate'];
|
||||
final time = collectionData['data']['collectionTime'] ?? '00:00';
|
||||
return DateFormat('yyyy-MM-dd HH:mm').parse('$date $time');
|
||||
}
|
||||
|
||||
debugPrint('No valid collection date found in data: ${collectionData.keys}');
|
||||
return DateTime.now();
|
||||
} catch (e) {
|
||||
debugPrint('Error parsing collection date: $e');
|
||||
return DateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
String _getStatusMessage(Map<String, dynamic> log) {
|
||||
if (log['status'] == 'S2' || log['status'] == 'S3') {
|
||||
return 'Successfully submitted to server';
|
||||
} else if (log['status'] == 'L1' || log['status'] == 'L3') {
|
||||
return 'Saved locally (pending submission)';
|
||||
} else if (log['status'] == 'L4') {
|
||||
return 'Partial submission (images failed)';
|
||||
}
|
||||
return 'Submission status unknown';
|
||||
}
|
||||
|
||||
void _filterLogs() {
|
||||
final installationQuery = _searchControllers['Installation']?.text.toLowerCase() ?? '';
|
||||
final collectionQuery = _searchControllers['Collection']?.text.toLowerCase() ?? '';
|
||||
|
||||
setState(() {
|
||||
_filteredInstallationLogs = _installationLogs.where((log) => _logMatchesQuery(log, installationQuery)).toList();
|
||||
_filteredCollectionLogs = _collectionLogs.where((log) => _logMatchesQuery(log, collectionQuery)).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);
|
||||
}
|
||||
|
||||
Future<void> _resubmitData(SubmissionLogEntry log) async {
|
||||
final logKey = log.reportId ?? log.submissionDateTime.toIso8601String();
|
||||
if (mounted) {
|
||||
setState(() => _isResubmitting[logKey] = true);
|
||||
}
|
||||
|
||||
try {
|
||||
final logData = log.rawData;
|
||||
final result = log.type == 'Installation'
|
||||
? await _submitInstallation(logData)
|
||||
: await _submitCollection(logData);
|
||||
|
||||
await _localStorageService.saveAirSamplingRecord(logData, logData['refID']);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Resubmission successful!')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Resubmission failed: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isResubmitting.remove(logKey));
|
||||
await _loadAllLogs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _submitInstallation(Map<String, dynamic> data) async {
|
||||
final dataToResubmit = AirInstallationData.fromJson(data);
|
||||
final result = await _apiService.post('air/manual/installation', dataToResubmit.toJsonForApi());
|
||||
|
||||
final imageFiles = dataToResubmit.getImagesForUpload();
|
||||
if (imageFiles.isNotEmpty && result['success'] == true) {
|
||||
await _apiService.air.uploadInstallationImages(
|
||||
airManId: result['data']['air_man_id'].toString(),
|
||||
files: imageFiles,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _submitCollection(Map<String, dynamic> data) async {
|
||||
final collectionData = data['collectionData'] ?? {};
|
||||
final dataToResubmit = AirCollectionData.fromMap(collectionData);
|
||||
final result = await _apiService.post('air/manual/collection', dataToResubmit.toJson());
|
||||
|
||||
final imageFiles = dataToResubmit.getImagesForUpload();
|
||||
if (imageFiles.isNotEmpty && result['success'] == true) {
|
||||
await _apiService.air.uploadCollectionImages(
|
||||
airManId: dataToResubmit.airManId.toString(),
|
||||
files: imageFiles,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasAnyLogs = _installationLogs.isNotEmpty || _collectionLogs.isNotEmpty;
|
||||
final hasFilteredLogs = _filteredInstallationLogs.isNotEmpty || _filteredCollectionLogs.isNotEmpty;
|
||||
|
||||
final logCategories = {
|
||||
'Installation': _filteredInstallationLogs,
|
||||
'Collection': _filteredCollectionLogs,
|
||||
};
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text("Air Manual Data Status Log")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: DataTable(
|
||||
columns: [
|
||||
DataColumn(label: Text("Date")),
|
||||
DataColumn(label: Text("Status")),
|
||||
DataColumn(label: Text("User")),
|
||||
appBar: AppBar(
|
||||
title: const Text('Air Sampling Status Log'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _loadAllLogs,
|
||||
tooltip: 'Refresh logs',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: RefreshIndicator(
|
||||
onRefresh: _loadAllLogs,
|
||||
child: !hasAnyLogs
|
||||
? const Center(child: Text('No air sampling logs found.'))
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
children: [
|
||||
...logCategories.entries
|
||||
.where((entry) => entry.value.isNotEmpty)
|
||||
.map((entry) => _buildCategorySection(entry.key, entry.value)),
|
||||
if (!hasFilteredLogs && hasAnyLogs)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Text('No logs match your search.'),
|
||||
),
|
||||
)
|
||||
],
|
||||
rows: logEntries.map((entry) {
|
||||
return DataRow(cells: [
|
||||
DataCell(Text(entry["Date"]!)),
|
||||
DataCell(Text(entry["Status"]!)),
|
||||
DataCell(Text(entry["User"]!)),
|
||||
]);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategorySection(String category, List<SubmissionLogEntry> logs) {
|
||||
final listHeight = (logs.length > 3 ? 3.5 : logs.length.toDouble()) * 75.0;
|
||||
|
||||
return Card(
|
||||
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 $category logs...',
|
||||
prefixIcon: const Icon(Icons.search, size: 20),
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
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,
|
||||
itemCount: logs.length,
|
||||
itemBuilder: (context, index) => _buildLogListItem(logs[index]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogListItem(SubmissionLogEntry log) {
|
||||
final isSuccess = log.status == 'S2' || log.status == 'S3';
|
||||
final logKey = log.reportId ?? log.submissionDateTime.toIso8601String();
|
||||
final isResubmitting = _isResubmitting[logKey] ?? false;
|
||||
final title = log.title;
|
||||
final subtitle = DateFormat('yyyy-MM-dd HH:mm').format(log.submissionDateTime);
|
||||
|
||||
return ExpansionTile(
|
||||
leading: Icon(
|
||||
isSuccess ? Icons.check_circle_outline : Icons.error_outline,
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
),
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
subtitle: Text(subtitle),
|
||||
trailing: !isSuccess
|
||||
? (isResubmitting
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 3))
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.sync, color: Colors.blue),
|
||||
onPressed: () => _resubmitData(log),
|
||||
))
|
||||
: null,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDetailRow('Station Code:', log.stationCode),
|
||||
if (log.reportId != null) _buildDetailRow('Report ID:', log.reportId!),
|
||||
_buildDetailRow('Status:', log.message),
|
||||
_buildDetailRow('Type:', '${log.type} Sampling'),
|
||||
if (log.type == 'Collection')
|
||||
_buildDetailRow('Installation ID:', log.rawData['installationRefID']?.toString() ?? 'N/A'),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('$label ', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Expanded(child: Text(value)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2,13 +2,12 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:environment_monitoring_app/models/tarball_data.dart';
|
||||
import 'package:environment_monitoring_app/models/in_situ_sampling_data.dart'; // Import In-Situ model
|
||||
import 'package:environment_monitoring_app/models/in_situ_sampling_data.dart';
|
||||
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
||||
import 'package:environment_monitoring_app/services/marine_api_service.dart';
|
||||
|
||||
// A unified model to represent any type of submission log entry.
|
||||
class SubmissionLogEntry {
|
||||
final String type; // e.g., 'tarball', 'in-situ'
|
||||
final String type; // e.g., 'Manual Sampling', 'Tarball Sampling'
|
||||
final String title;
|
||||
final String stationCode;
|
||||
final DateTime submissionDateTime;
|
||||
@ -42,40 +41,64 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
final LocalStorageService _localStorageService = LocalStorageService();
|
||||
final MarineApiService _marineApiService = MarineApiService();
|
||||
|
||||
Map<String, List<SubmissionLogEntry>> _groupedLogs = {};
|
||||
Map<String, List<SubmissionLogEntry>> _filteredLogs = {};
|
||||
final Map<String, bool> _isCategoryExpanded = {};
|
||||
// Raw data lists
|
||||
List<SubmissionLogEntry> _manualLogs = [];
|
||||
List<SubmissionLogEntry> _tarballLogs = [];
|
||||
|
||||
// Filtered lists for the UI
|
||||
List<SubmissionLogEntry> _filteredManualLogs = [];
|
||||
List<SubmissionLogEntry> _filteredTarballLogs = [];
|
||||
|
||||
// Per-category search controllers
|
||||
final Map<String, TextEditingController> _searchControllers = {};
|
||||
|
||||
bool _isLoading = true;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final Map<String, bool> _isResubmitting = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchControllers['Manual Sampling'] = TextEditingController()..addListener(_filterLogs);
|
||||
_searchControllers['Tarball Sampling'] = TextEditingController()..addListener(_filterLogs);
|
||||
_loadAllLogs();
|
||||
_searchController.addListener(_filterLogs);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_searchControllers['Manual Sampling']?.dispose();
|
||||
_searchControllers['Tarball Sampling']?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Loads logs from all available modules (Tarball, In-Situ, etc.)
|
||||
Future<void> _loadAllLogs() async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
// --- Fetch logs for all types ---
|
||||
final tarballLogs = await _localStorageService.getAllTarballLogs();
|
||||
final inSituLogs = await _localStorageService.getAllInSituLogs();
|
||||
|
||||
final Map<String, List<SubmissionLogEntry>> tempGroupedLogs = {};
|
||||
final List<SubmissionLogEntry> tempManual = [];
|
||||
final List<SubmissionLogEntry> tempTarball = [];
|
||||
|
||||
// Map tarball logs (Unchanged)
|
||||
final List<SubmissionLogEntry> tarballEntries = [];
|
||||
// Map In-Situ logs to Manual Sampling
|
||||
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'] ?? '';
|
||||
|
||||
tempManual.add(SubmissionLogEntry(
|
||||
type: 'Manual Sampling',
|
||||
title: log['selectedStation']?['man_station_name'] ?? 'Unknown Station',
|
||||
stationCode: log['selectedStation']?['man_station_code'] ?? 'N/A',
|
||||
submissionDateTime: DateTime.tryParse('$dateStr $timeStr') ?? DateTime.now(),
|
||||
reportId: log['reportId']?.toString(),
|
||||
status: log['submissionStatus'] ?? 'L1',
|
||||
message: log['submissionMessage'] ?? 'No status message.',
|
||||
rawData: log,
|
||||
));
|
||||
}
|
||||
|
||||
// Map Tarball logs
|
||||
for (var log in tarballLogs) {
|
||||
tarballEntries.add(SubmissionLogEntry(
|
||||
tempTarball.add(SubmissionLogEntry(
|
||||
type: 'Tarball Sampling',
|
||||
title: log['selectedStation']?['tbl_station_name'] ?? 'Unknown Station',
|
||||
stationCode: log['selectedStation']?['tbl_station_code'] ?? 'N/A',
|
||||
@ -86,133 +109,84 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
rawData: log,
|
||||
));
|
||||
}
|
||||
if (tarballEntries.isNotEmpty) {
|
||||
tarballEntries.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
|
||||
tempGroupedLogs['Tarball Sampling'] = tarballEntries;
|
||||
}
|
||||
|
||||
// --- Map In-Situ logs ---
|
||||
final List<SubmissionLogEntry> inSituEntries = [];
|
||||
for (var log in inSituLogs) {
|
||||
// REPAIRED: Use the correct date/time keys for In-Situ data.
|
||||
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',
|
||||
title: log['selectedStation']?['man_station_name'] ?? 'Unknown Station',
|
||||
stationCode: log['selectedStation']?['man_station_code'] ?? 'N/A',
|
||||
submissionDateTime: DateTime.tryParse('$dateStr $timeStr') ?? DateTime.now(),
|
||||
reportId: log['reportId']?.toString(),
|
||||
status: log['submissionStatus'] ?? 'L1',
|
||||
message: log['submissionMessage'] ?? 'No status message.',
|
||||
rawData: log,
|
||||
));
|
||||
}
|
||||
if (inSituEntries.isNotEmpty) {
|
||||
inSituEntries.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
|
||||
tempGroupedLogs['In-Situ Sampling'] = inSituEntries;
|
||||
}
|
||||
// --- END of In-Situ mapping ---
|
||||
tempManual.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
|
||||
tempTarball.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_groupedLogs = tempGroupedLogs;
|
||||
_filteredLogs = tempGroupedLogs;
|
||||
_manualLogs = tempManual;
|
||||
_tarballLogs = tempTarball;
|
||||
_isLoading = false;
|
||||
});
|
||||
_filterLogs(); // Perform initial filter
|
||||
}
|
||||
}
|
||||
|
||||
void _filterLogs() {
|
||||
final query = _searchController.text.toLowerCase();
|
||||
final Map<String, List<SubmissionLogEntry>> tempFiltered = {};
|
||||
final manualQuery = _searchControllers['Manual Sampling']?.text.toLowerCase() ?? '';
|
||||
final tarballQuery = _searchControllers['Tarball Sampling']?.text.toLowerCase() ?? '';
|
||||
|
||||
_groupedLogs.forEach((category, logs) {
|
||||
final filtered = logs.where((log) {
|
||||
setState(() {
|
||||
_filteredManualLogs = _manualLogs.where((log) => _logMatchesQuery(log, manualQuery)).toList();
|
||||
_filteredTarballLogs = _tarballLogs.where((log) => _logMatchesQuery(log, tarballQuery)).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) {
|
||||
case 'Tarball Sampling':
|
||||
await _resubmitTarballData(log);
|
||||
break;
|
||||
case 'In-Situ Sampling':
|
||||
await _resubmitInSituData(log);
|
||||
break;
|
||||
default:
|
||||
final logKey = log.reportId ?? log.submissionDateTime.toIso8601String();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Resubmission for '${log.type}' is not implemented."), backgroundColor: Colors.orange),
|
||||
);
|
||||
setState(() => log.isResubmitting = false);
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_isResubmitting[logKey] = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// Handles resubmission for Tarball data. (Unchanged)
|
||||
Future<void> _resubmitTarballData(SubmissionLogEntry log) async {
|
||||
try {
|
||||
final result = await _performResubmission(log);
|
||||
final logData = log.rawData;
|
||||
|
||||
final int? firstSamplerId = int.tryParse(logData['first_sampler_user_id']?.toString() ?? '');
|
||||
final int? classificationId = int.tryParse(logData['classification_id']?.toString() ?? '');
|
||||
|
||||
final TarballSamplingData dataToResubmit = TarballSamplingData()
|
||||
..selectedStation = logData['selectedStation']
|
||||
..samplingDate = logData['sampling_date']
|
||||
..samplingTime = logData['sampling_time']
|
||||
..firstSamplerUserId = firstSamplerId
|
||||
..secondSampler = logData['secondSampler']
|
||||
..classificationId = classificationId
|
||||
..currentLatitude = logData['current_latitude']?.toString()
|
||||
..currentLongitude = logData['current_longitude']?.toString()
|
||||
..distanceDifference = logData['distance_difference']
|
||||
..optionalRemark1 = logData['optional_photo_remark_01']
|
||||
..optionalRemark2 = logData['optional_photo_remark_02']
|
||||
..optionalRemark3 = logData['optional_photo_remark_03']
|
||||
..optionalRemark4 = logData['optional_photo_remark_04'];
|
||||
|
||||
final Map<String, File?> imageFiles = {};
|
||||
final imageKeys = ['left_side_coastal_view', 'right_side_coastal_view', 'drawing_vertical_lines', 'drawing_horizontal_line', 'optional_photo_01', 'optional_photo_02', 'optional_photo_03', 'optional_photo_04'];
|
||||
for (var key in imageKeys) {
|
||||
final imagePath = logData[key];
|
||||
if (imagePath != null && imagePath.isNotEmpty) {
|
||||
final file = File(imagePath);
|
||||
if (await file.exists()) imageFiles[key] = file;
|
||||
}
|
||||
}
|
||||
|
||||
final result = await _marineApiService.submitTarballSample(formData: dataToResubmit.toFormData(), imageFiles: imageFiles);
|
||||
|
||||
logData['submissionStatus'] = result['status'];
|
||||
logData['submissionMessage'] = result['message'];
|
||||
logData['reportId'] = result['reportId']?.toString() ?? logData['reportId'];
|
||||
await _localStorageService.updateTarballLog(logData);
|
||||
|
||||
if (mounted) await _loadAllLogs();
|
||||
if (log.type == 'Manual Sampling') {
|
||||
await _localStorageService.updateInSituLog(logData);
|
||||
} else if (log.type == 'Tarball Sampling') {
|
||||
await _localStorageService.updateTarballLog(logData);
|
||||
}
|
||||
|
||||
/// Handles resubmission for In-Situ data. (Unchanged)
|
||||
Future<void> _resubmitInSituData(SubmissionLogEntry log) async {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Resubmission successful!')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Resubmission failed: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isResubmitting.remove(logKey);
|
||||
});
|
||||
await _loadAllLogs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _performResubmission(SubmissionLogEntry log) async {
|
||||
final logData = log.rawData;
|
||||
|
||||
// Reconstruct the InSituSamplingData object from the raw map
|
||||
if (log.type == 'Manual Sampling') {
|
||||
final InSituSamplingData dataToResubmit = InSituSamplingData()
|
||||
..firstSamplerUserId = int.tryParse(logData['first_sampler_user_id']?.toString() ?? '')
|
||||
..secondSampler = logData['secondSampler']
|
||||
@ -247,9 +221,7 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
..tss = double.tryParse(logData['tss_mg_l']?.toString() ?? '0.0')
|
||||
..batteryVoltage = double.tryParse(logData['battery_v']?.toString() ?? '0.0');
|
||||
|
||||
// Reconstruct image files
|
||||
final Map<String, File?> imageFiles = {};
|
||||
// Use the keys from the model to ensure consistency
|
||||
final imageKeys = dataToResubmit.toApiImageFiles().keys;
|
||||
for (var key in imageKeys) {
|
||||
final imagePath = logData[key];
|
||||
@ -261,89 +233,119 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the data via the API service
|
||||
final result = await _marineApiService.submitInSituSample(
|
||||
return _marineApiService.submitInSituSample(
|
||||
formData: dataToResubmit.toApiFormData(),
|
||||
imageFiles: imageFiles,
|
||||
);
|
||||
} else if (log.type == 'Tarball Sampling') {
|
||||
final int? firstSamplerId = int.tryParse(logData['first_sampler_user_id']?.toString() ?? '');
|
||||
final int? classificationId = int.tryParse(logData['classification_id']?.toString() ?? '');
|
||||
|
||||
// Update the local log file with the new submission status
|
||||
logData['submissionStatus'] = result['status'];
|
||||
logData['submissionMessage'] = result['message'];
|
||||
logData['reportId'] = result['reportId']?.toString() ?? logData['reportId'];
|
||||
final TarballSamplingData dataToResubmit = TarballSamplingData()
|
||||
..selectedStation = logData['selectedStation']
|
||||
..samplingDate = logData['sampling_date']
|
||||
..samplingTime = logData['sampling_time']
|
||||
..firstSamplerUserId = firstSamplerId
|
||||
..secondSampler = logData['secondSampler']
|
||||
..classificationId = classificationId
|
||||
..currentLatitude = logData['current_latitude']?.toString()
|
||||
..currentLongitude = logData['current_longitude']?.toString()
|
||||
..distanceDifference = logData['distance_difference']
|
||||
..optionalRemark1 = logData['optional_photo_remark_01']
|
||||
..optionalRemark2 = logData['optional_photo_remark_02']
|
||||
..optionalRemark3 = logData['optional_photo_remark_03']
|
||||
..optionalRemark4 = logData['optional_photo_remark_04'];
|
||||
|
||||
// Use the correct update method for In-Situ logs
|
||||
await _localStorageService.updateInSituLog(logData);
|
||||
final Map<String, File?> imageFiles = {};
|
||||
final imageKeys = ['left_side_coastal_view', 'right_side_coastal_view', 'drawing_vertical_lines', 'drawing_horizontal_line', 'optional_photo_01', 'optional_photo_02', 'optional_photo_03', 'optional_photo_04'];
|
||||
for (var key in imageKeys) {
|
||||
final imagePath = logData[key];
|
||||
if (imagePath != null && imagePath.isNotEmpty) {
|
||||
final file = File(imagePath);
|
||||
if (await file.exists()) imageFiles[key] = file;
|
||||
}
|
||||
}
|
||||
|
||||
// Reload logs to refresh the UI
|
||||
if (mounted) await _loadAllLogs();
|
||||
return _marineApiService.submitTarballSample(formData: dataToResubmit.toFormData(), imageFiles: imageFiles);
|
||||
}
|
||||
|
||||
throw Exception('Unknown submission type: ${log.type}');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasAnyLogs = _manualLogs.isNotEmpty || _tarballLogs.isNotEmpty;
|
||||
final hasFilteredLogs = _filteredManualLogs.isNotEmpty || _filteredTarballLogs.isNotEmpty;
|
||||
|
||||
final logCategories = {
|
||||
'Manual Sampling': _filteredManualLogs,
|
||||
'Tarball Sampling': _filteredTarballLogs,
|
||||
};
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Marine Manual 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
|
||||
appBar: AppBar(title: const Text('Marine Data Status Log')),
|
||||
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: [
|
||||
...logCategories.entries
|
||||
.where((entry) => entry.value.isNotEmpty)
|
||||
.map((entry) => _buildCategorySection(entry.key, entry.value)),
|
||||
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);
|
||||
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)'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -353,6 +355,8 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
|
||||
Widget _buildLogListItem(SubmissionLogEntry log) {
|
||||
final isFailed = log.status != 'L3';
|
||||
final logKey = log.reportId ?? log.submissionDateTime.toIso8601String();
|
||||
final isResubmitting = _isResubmitting[logKey] ?? false;
|
||||
final title = '${log.title} (${log.stationCode})';
|
||||
final subtitle = DateFormat('yyyy-MM-dd HH:mm').format(log.submissionDateTime);
|
||||
|
||||
@ -364,21 +368,13 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
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),
|
||||
))
|
||||
? (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)))
|
||||
: 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: [
|
||||
|
||||
@ -55,18 +55,22 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
||||
final Map<String, TextEditingController> _searchControllers = {};
|
||||
|
||||
bool _isLoading = true;
|
||||
final Map<String, bool> _isResubmitting = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchControllers['Schedule'] = TextEditingController()..addListener(_filterLogs);
|
||||
_searchControllers['Triennial'] = TextEditingController()..addListener(_filterLogs);
|
||||
_searchControllers['Others'] = TextEditingController()..addListener(_filterLogs);
|
||||
_loadAllLogs();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var controller in _searchControllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
_searchControllers['Schedule']?.dispose();
|
||||
_searchControllers['Triennial']?.dispose();
|
||||
_searchControllers['Others']?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -108,18 +112,6 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
||||
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(() {
|
||||
_scheduleLogs = tempSchedule;
|
||||
@ -151,7 +143,14 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
||||
}
|
||||
|
||||
Future<void> _resubmitData(SubmissionLogEntry log) async {
|
||||
setState(() => log.isResubmitting = true);
|
||||
final logKey = log.reportId ?? log.submissionDateTime.toIso8601String();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isResubmitting[logKey] = true;
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
final logData = log.rawData;
|
||||
final dataToResubmit = RiverInSituSamplingData.fromJson(logData);
|
||||
final Map<String, File?> imageFiles = {};
|
||||
@ -171,11 +170,31 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
||||
formData: dataToResubmit.toApiFormData(),
|
||||
imageFiles: imageFiles,
|
||||
);
|
||||
|
||||
logData['submissionStatus'] = result['status'];
|
||||
logData['submissionMessage'] = result['message'];
|
||||
logData['reportId'] = result['reportId']?.toString() ?? logData['reportId'];
|
||||
await _localStorageService.updateRiverInSituLog(logData);
|
||||
if (mounted) await _loadAllLogs();
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Resubmission successful!')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Resubmission failed: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isResubmitting.remove(logKey);
|
||||
});
|
||||
await _loadAllLogs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -183,6 +202,12 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
||||
final hasAnyLogs = _scheduleLogs.isNotEmpty || _triennialLogs.isNotEmpty || _otherLogs.isNotEmpty;
|
||||
final hasFilteredLogs = _filteredScheduleLogs.isNotEmpty || _filteredTriennialLogs.isNotEmpty || _filteredOtherLogs.isNotEmpty;
|
||||
|
||||
final logCategories = {
|
||||
'Schedule': _filteredScheduleLogs,
|
||||
'Triennial': _filteredTriennialLogs,
|
||||
'Others': _filteredOtherLogs,
|
||||
};
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('River Data Status Log')),
|
||||
body: _isLoading
|
||||
@ -194,13 +219,9 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
||||
: ListView(
|
||||
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),
|
||||
...logCategories.entries
|
||||
.where((entry) => entry.value.isNotEmpty)
|
||||
.map((entry) => _buildCategorySection(entry.key, entry.value)),
|
||||
if (!hasFilteredLogs && hasAnyLogs)
|
||||
const Center(
|
||||
child: Padding(
|
||||
@ -215,8 +236,6 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
||||
}
|
||||
|
||||
Widget _buildCategorySection(String category, List<SubmissionLogEntry> logs) {
|
||||
// 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(
|
||||
@ -262,6 +281,8 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
||||
|
||||
Widget _buildLogListItem(SubmissionLogEntry log) {
|
||||
final isFailed = log.status != 'L3';
|
||||
final logKey = log.reportId ?? log.submissionDateTime.toIso8601String();
|
||||
final isResubmitting = _isResubmitting[logKey] ?? false;
|
||||
final title = '${log.title} (${log.stationCode})';
|
||||
final subtitle = DateFormat('yyyy-MM-dd HH:mm').format(log.submissionDateTime);
|
||||
|
||||
@ -273,7 +294,7 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
subtitle: Text(subtitle),
|
||||
trailing: isFailed
|
||||
? (log.isResubmitting
|
||||
? (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)))
|
||||
: null,
|
||||
|
||||
@ -25,7 +25,8 @@ class RiverApiService {
|
||||
required Map<String, String> formData,
|
||||
required Map<String, File?> imageFiles,
|
||||
}) async {
|
||||
// --- Step 1: Submit Form Data ---
|
||||
// --- Step 1: Submit Form Data as JSON ---
|
||||
// The PHP backend for submitInSituSample expects JSON input.
|
||||
final dataResult = await _baseService.post('river/manual/sample', formData);
|
||||
|
||||
if (dataResult['success'] != true) {
|
||||
@ -64,8 +65,8 @@ class RiverApiService {
|
||||
}
|
||||
|
||||
final imageResult = await _baseService.postMultipart(
|
||||
endpoint: 'river/manual/images',
|
||||
fields: {'r_man_id': recordId.toString()},
|
||||
endpoint: 'river/manual/images', // Separate endpoint for images
|
||||
fields: {'r_man_id': recordId.toString()}, // Link images to the submitted record ID
|
||||
files: filesToUpload,
|
||||
);
|
||||
|
||||
@ -111,7 +112,6 @@ class RiverApiService {
|
||||
..writeln('*Status of Submission:* Successful');
|
||||
|
||||
if (distanceKm > 0 || (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A')) {
|
||||
// CORRECTED: The cascade operator '..' was missing on the next line.
|
||||
buffer
|
||||
..writeln()
|
||||
..writeln('🔔 *Alert:*')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user