fix air manual data status log

This commit is contained in:
ALim Aidrus 2025-08-14 16:18:56 +08:00
parent f7fefd3f45
commit a2d8b372e6
6 changed files with 736 additions and 316 deletions

View File

@ -1,4 +1,3 @@
// lib/models/air_collection_data.dart
import 'dart:io'; import 'dart:io';
class AirCollectionData { class AirCollectionData {
@ -189,7 +188,6 @@ class AirCollectionData {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'air_man_id': airManId?.toString(), '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_user_id': collectionUserId?.toString(),
'air_man_collection_date': collectionDate, 'air_man_collection_date': collectionDate,
'air_man_collection_time': collectionTime, 'air_man_collection_time': collectionTime,

View File

@ -91,9 +91,17 @@ class RiverInSituSamplingData {
return null; 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() return RiverInSituSamplingData()
..firstSamplerName = json['first_sampler_name'] ..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'] ..secondSampler = json['secondSampler']
..samplingDate = json['r_man_date'] ..samplingDate = json['r_man_date']
..samplingTime = json['r_man_time'] ..samplingTime = json['r_man_time']
@ -226,4 +234,4 @@ class RiverInSituSamplingData {
'r_man_optional_photo_04': optionalImage4, 'r_man_optional_photo_04': optionalImage4,
}; };
} }
} }

View File

@ -1,32 +1,429 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class AirManualDataStatusLog extends StatelessWidget { import '../../../../models/air_installation_data.dart';
final List<Map<String, String>> logEntries = [ import '../../../../models/air_collection_data.dart';
{"Date": "2025-07-15", "Status": "Submitted", "User": "analyst_air"}, import '../../../../services/local_storage_service.dart';
{"Date": "2025-07-16", "Status": "Approved", "User": "supervisor_air"}, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final hasAnyLogs = _installationLogs.isNotEmpty || _collectionLogs.isNotEmpty;
final hasFilteredLogs = _filteredInstallationLogs.isNotEmpty || _filteredCollectionLogs.isNotEmpty;
final logCategories = {
'Installation': _filteredInstallationLogs,
'Collection': _filteredCollectionLogs,
};
return Scaffold( return Scaffold(
appBar: AppBar(title: Text("Air Manual Data Status Log")), appBar: AppBar(
body: Padding( title: const Text('Air Sampling Status Log'),
padding: const EdgeInsets.all(24), actions: [
child: DataTable( IconButton(
columns: [ icon: const Icon(Icons.refresh),
DataColumn(label: Text("Date")), onPressed: _loadAllLogs,
DataColumn(label: Text("Status")), tooltip: 'Refresh logs',
DataColumn(label: Text("User")), ),
],
),
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)),
],
),
);
}
} }

View File

@ -2,13 +2,12 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:environment_monitoring_app/models/tarball_data.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/local_storage_service.dart';
import 'package:environment_monitoring_app/services/marine_api_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 { class SubmissionLogEntry {
final String type; // e.g., 'tarball', 'in-situ' final String type; // e.g., 'Manual Sampling', 'Tarball Sampling'
final String title; final String title;
final String stationCode; final String stationCode;
final DateTime submissionDateTime; final DateTime submissionDateTime;
@ -42,40 +41,64 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
final LocalStorageService _localStorageService = LocalStorageService(); final LocalStorageService _localStorageService = LocalStorageService();
final MarineApiService _marineApiService = MarineApiService(); final MarineApiService _marineApiService = MarineApiService();
Map<String, List<SubmissionLogEntry>> _groupedLogs = {}; // Raw data lists
Map<String, List<SubmissionLogEntry>> _filteredLogs = {}; List<SubmissionLogEntry> _manualLogs = [];
final Map<String, bool> _isCategoryExpanded = {}; 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; bool _isLoading = true;
final TextEditingController _searchController = TextEditingController(); final Map<String, bool> _isResubmitting = {};
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_searchControllers['Manual Sampling'] = TextEditingController()..addListener(_filterLogs);
_searchControllers['Tarball Sampling'] = TextEditingController()..addListener(_filterLogs);
_loadAllLogs(); _loadAllLogs();
_searchController.addListener(_filterLogs);
} }
@override @override
void dispose() { void dispose() {
_searchController.dispose(); _searchControllers['Manual Sampling']?.dispose();
_searchControllers['Tarball Sampling']?.dispose();
super.dispose(); super.dispose();
} }
/// Loads logs from all available modules (Tarball, In-Situ, etc.)
Future<void> _loadAllLogs() async { Future<void> _loadAllLogs() async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
// --- Fetch logs for all types ---
final tarballLogs = await _localStorageService.getAllTarballLogs(); final tarballLogs = await _localStorageService.getAllTarballLogs();
final inSituLogs = await _localStorageService.getAllInSituLogs(); final inSituLogs = await _localStorageService.getAllInSituLogs();
final Map<String, List<SubmissionLogEntry>> tempGroupedLogs = {}; final List<SubmissionLogEntry> tempManual = [];
final List<SubmissionLogEntry> tempTarball = [];
// Map tarball logs (Unchanged) // Map In-Situ logs to Manual Sampling
final List<SubmissionLogEntry> tarballEntries = []; 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) { for (var log in tarballLogs) {
tarballEntries.add(SubmissionLogEntry( tempTarball.add(SubmissionLogEntry(
type: 'Tarball Sampling', type: 'Tarball Sampling',
title: log['selectedStation']?['tbl_station_name'] ?? 'Unknown Station', title: log['selectedStation']?['tbl_station_name'] ?? 'Unknown Station',
stationCode: log['selectedStation']?['tbl_station_code'] ?? 'N/A', stationCode: log['selectedStation']?['tbl_station_code'] ?? 'N/A',
@ -86,265 +109,244 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
rawData: log, rawData: log,
)); ));
} }
if (tarballEntries.isNotEmpty) {
tarballEntries.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
tempGroupedLogs['Tarball Sampling'] = tarballEntries;
}
// --- Map In-Situ logs --- tempManual.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
final List<SubmissionLogEntry> inSituEntries = []; tempTarball.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
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 ---
if (mounted) { if (mounted) {
setState(() { setState(() {
_groupedLogs = tempGroupedLogs; _manualLogs = tempManual;
_filteredLogs = tempGroupedLogs; _tarballLogs = tempTarball;
_isLoading = false; _isLoading = false;
}); });
_filterLogs(); // Perform initial filter
} }
} }
void _filterLogs() { void _filterLogs() {
final query = _searchController.text.toLowerCase(); final manualQuery = _searchControllers['Manual Sampling']?.text.toLowerCase() ?? '';
final Map<String, List<SubmissionLogEntry>> tempFiltered = {}; final tarballQuery = _searchControllers['Tarball Sampling']?.text.toLowerCase() ?? '';
_groupedLogs.forEach((category, logs) {
final filtered = logs.where((log) {
return log.title.toLowerCase().contains(query) ||
log.stationCode.toLowerCase().contains(query) ||
(log.reportId?.toLowerCase() ?? '').contains(query);
}).toList();
if (filtered.isNotEmpty) {
tempFiltered[category] = filtered;
}
});
setState(() { setState(() {
_filteredLogs = tempFiltered; _filteredManualLogs = _manualLogs.where((log) => _logMatchesQuery(log, manualQuery)).toList();
_filteredTarballLogs = _tarballLogs.where((log) => _logMatchesQuery(log, tarballQuery)).toList();
}); });
} }
/// Main router for resubmitting data based on its type. 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 { Future<void> _resubmitData(SubmissionLogEntry log) async {
setState(() => log.isResubmitting = true); final logKey = log.reportId ?? log.submissionDateTime.toIso8601String();
if (mounted) {
setState(() {
_isResubmitting[logKey] = true;
});
}
switch (log.type) { try {
case 'Tarball Sampling': final result = await _performResubmission(log);
await _resubmitTarballData(log); final logData = log.rawData;
break;
case 'In-Situ Sampling': logData['submissionStatus'] = result['status'];
await _resubmitInSituData(log); logData['submissionMessage'] = result['message'];
break; logData['reportId'] = result['reportId']?.toString() ?? logData['reportId'];
default:
if (mounted) { if (log.type == 'Manual Sampling') {
ScaffoldMessenger.of(context).showSnackBar( await _localStorageService.updateInSituLog(logData);
SnackBar(content: Text("Resubmission for '${log.type}' is not implemented."), backgroundColor: Colors.orange), } else if (log.type == 'Tarball Sampling') {
); await _localStorageService.updateTarballLog(logData);
setState(() => log.isResubmitting = false); }
}
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();
}
} }
} }
/// Handles resubmission for Tarball data. (Unchanged) Future<Map<String, dynamic>> _performResubmission(SubmissionLogEntry log) async {
Future<void> _resubmitTarballData(SubmissionLogEntry log) async {
final logData = log.rawData; final logData = log.rawData;
final int? firstSamplerId = int.tryParse(logData['first_sampler_user_id']?.toString() ?? ''); if (log.type == 'Manual Sampling') {
final int? classificationId = int.tryParse(logData['classification_id']?.toString() ?? ''); final InSituSamplingData dataToResubmit = InSituSamplingData()
..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']
..tideLevel = logData['tide_level']
..seaCondition = logData['sea_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 TarballSamplingData dataToResubmit = TarballSamplingData() final Map<String, File?> imageFiles = {};
..selectedStation = logData['selectedStation'] final imageKeys = dataToResubmit.toApiImageFiles().keys;
..samplingDate = logData['sampling_date'] for (var key in imageKeys) {
..samplingTime = logData['sampling_time'] final imagePath = logData[key];
..firstSamplerUserId = firstSamplerId if (imagePath is String && imagePath.isNotEmpty) {
..secondSampler = logData['secondSampler'] final file = File(imagePath);
..classificationId = classificationId if (await file.exists()) {
..currentLatitude = logData['current_latitude']?.toString() imageFiles[key] = file;
..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();
}
/// Handles resubmission for In-Situ data. (Unchanged)
Future<void> _resubmitInSituData(SubmissionLogEntry log) async {
final logData = log.rawData;
// Reconstruct the InSituSamplingData object from the raw map
final InSituSamplingData dataToResubmit = InSituSamplingData()
..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']
..tideLevel = logData['tide_level']
..seaCondition = logData['sea_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');
// 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];
if (imagePath is String && imagePath.isNotEmpty) {
final file = File(imagePath);
if (await file.exists()) {
imageFiles[key] = file;
} }
} }
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() ?? '');
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;
}
}
return _marineApiService.submitTarballSample(formData: dataToResubmit.toFormData(), imageFiles: imageFiles);
} }
// Submit the data via the API service throw Exception('Unknown submission type: ${log.type}');
final result = await _marineApiService.submitInSituSample(
formData: dataToResubmit.toApiFormData(),
imageFiles: imageFiles,
);
// 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'];
// Use the correct update method for In-Situ logs
await _localStorageService.updateInSituLog(logData);
// Reload logs to refresh the UI
if (mounted) await _loadAllLogs();
} }
@override @override
Widget build(BuildContext context) { 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( return Scaffold(
appBar: AppBar(title: const Text('Marine Manual Data Status Log')), appBar: AppBar(title: const Text('Marine Data Status Log')),
body: Column( body: _isLoading
children: [ ? const Center(child: CircularProgressIndicator())
Padding( : RefreshIndicator(
padding: const EdgeInsets.all(8.0), onRefresh: _loadAllLogs,
child: TextField( child: !hasAnyLogs
controller: _searchController, ? const Center(child: Text('No submission logs found.'))
decoration: InputDecoration( : ListView(
labelText: 'Search Logs...', padding: const EdgeInsets.all(8.0),
prefixIcon: const Icon(Icons.search), children: [
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.0)), ...logCategories.entries
), .where((entry) => entry.value.isNotEmpty)
), .map((entry) => _buildCategorySection(entry.key, entry.value)),
), if (!hasFilteredLogs && hasAnyLogs)
Expanded( const Center(
child: _isLoading child: Padding(
? const Center(child: CircularProgressIndicator()) padding: EdgeInsets.all(24.0),
: RefreshIndicator( child: Text('No logs match your search.'),
onRefresh: _loadAllLogs, ),
child: _filteredLogs.isEmpty )
? Center(child: Text(_groupedLogs.isEmpty ? 'No submission logs found.' : 'No logs match your search.')) ],
: ListView( ),
children: _filteredLogs.entries.map((entry) {
return _buildCategorySection(entry.key, entry.value);
}).toList(),
),
),
),
],
), ),
); );
} }
Widget _buildCategorySection(String category, List<SubmissionLogEntry> logs) { Widget _buildCategorySection(String category, List<SubmissionLogEntry> logs) {
final bool isExpanded = _isCategoryExpanded[category] ?? false; final listHeight = (logs.length > 5 ? 5.5 : logs.length.toDouble()) * 75.0;
final int itemCount = isExpanded ? logs.length : (logs.length > 5 ? 5 : logs.length);
return Card( return Card(
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(category, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), Text(category, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
const Divider(), Padding(
ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8.0),
shrinkWrap: true, child: TextField(
physics: const NeverScrollableScrollPhysics(), controller: _searchControllers[category],
itemCount: itemCount, decoration: InputDecoration(
itemBuilder: (context, index) { hintText: 'Search in $category...',
return _buildLogListItem(logs[index]); prefixIcon: const Icon(Icons.search, size: 20),
}, isDense: true,
), border: const OutlineInputBorder(),
if (logs.length > 5) ),
TextButton(
onPressed: () {
setState(() {
_isCategoryExpanded[category] = !isExpanded;
});
},
child: Text(isExpanded ? 'Show Less' : 'Show More (${logs.length - 5} more)'),
), ),
),
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) {
return _buildLogListItem(logs[index]);
},
),
),
], ],
), ),
), ),
@ -353,6 +355,8 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
Widget _buildLogListItem(SubmissionLogEntry log) { Widget _buildLogListItem(SubmissionLogEntry log) {
final isFailed = log.status != 'L3'; final isFailed = log.status != 'L3';
final logKey = log.reportId ?? log.submissionDateTime.toIso8601String();
final isResubmitting = _isResubmitting[logKey] ?? false;
final title = '${log.title} (${log.stationCode})'; final title = '${log.title} (${log.stationCode})';
final subtitle = DateFormat('yyyy-MM-dd HH:mm').format(log.submissionDateTime); 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)), title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(subtitle), subtitle: Text(subtitle),
trailing: isFailed trailing: isFailed
? (log.isResubmitting ? (isResubmitting
? const SizedBox( ? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 3))
height: 24, : IconButton(icon: const Icon(Icons.sync, color: Colors.blue), tooltip: 'Resubmit', onPressed: () => _resubmitData(log)))
width: 24,
child: CircularProgressIndicator(strokeWidth: 3),
)
: IconButton(
icon: const Icon(Icons.sync, color: Colors.blue),
tooltip: 'Resubmit',
onPressed: () => _resubmitData(log),
))
: null, : null,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@ -55,18 +55,22 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
final Map<String, TextEditingController> _searchControllers = {}; final Map<String, TextEditingController> _searchControllers = {};
bool _isLoading = true; bool _isLoading = true;
final Map<String, bool> _isResubmitting = {};
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_searchControllers['Schedule'] = TextEditingController()..addListener(_filterLogs);
_searchControllers['Triennial'] = TextEditingController()..addListener(_filterLogs);
_searchControllers['Others'] = TextEditingController()..addListener(_filterLogs);
_loadAllLogs(); _loadAllLogs();
} }
@override @override
void dispose() { void dispose() {
for (var controller in _searchControllers.values) { _searchControllers['Schedule']?.dispose();
controller.dispose(); _searchControllers['Triennial']?.dispose();
} _searchControllers['Others']?.dispose();
super.dispose(); super.dispose();
} }
@ -108,18 +112,6 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
tempTriennial.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)); tempOthers.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
// Initialize search controllers for categories that have data
final categories = {'Schedule': tempSchedule, 'Triennial': tempTriennial, 'Others': tempOthers};
categories.forEach((key, value) {
if (value.isNotEmpty) {
_searchControllers.putIfAbsent(key, () {
final controller = TextEditingController();
controller.addListener(() => _filterLogs());
return controller;
});
}
});
if (mounted) { if (mounted) {
setState(() { setState(() {
_scheduleLogs = tempSchedule; _scheduleLogs = tempSchedule;
@ -151,31 +143,58 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
} }
Future<void> _resubmitData(SubmissionLogEntry log) async { Future<void> _resubmitData(SubmissionLogEntry log) async {
setState(() => log.isResubmitting = true); final logKey = log.reportId ?? log.submissionDateTime.toIso8601String();
final logData = log.rawData; if (mounted) {
final dataToResubmit = RiverInSituSamplingData.fromJson(logData); setState(() {
final Map<String, File?> imageFiles = {}; _isResubmitting[logKey] = true;
});
final imageApiKeys = dataToResubmit.toApiImageFiles().keys;
for (var key in imageApiKeys) {
final imagePath = logData[key];
if (imagePath is String && imagePath.isNotEmpty) {
final file = File(imagePath);
if (await file.exists()) {
imageFiles[key] = file;
}
}
} }
final result = await _riverApiService.submitInSituSample( try {
formData: dataToResubmit.toApiFormData(), final logData = log.rawData;
imageFiles: imageFiles, final dataToResubmit = RiverInSituSamplingData.fromJson(logData);
); final Map<String, File?> imageFiles = {};
logData['submissionStatus'] = result['status'];
logData['submissionMessage'] = result['message']; final imageApiKeys = dataToResubmit.toApiImageFiles().keys;
logData['reportId'] = result['reportId']?.toString() ?? logData['reportId']; for (var key in imageApiKeys) {
await _localStorageService.updateRiverInSituLog(logData); final imagePath = logData[key];
if (mounted) await _loadAllLogs(); if (imagePath is String && imagePath.isNotEmpty) {
final file = File(imagePath);
if (await file.exists()) {
imageFiles[key] = file;
}
}
}
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'];
await _localStorageService.updateRiverInSituLog(logData);
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 @override
@ -183,6 +202,12 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
final hasAnyLogs = _scheduleLogs.isNotEmpty || _triennialLogs.isNotEmpty || _otherLogs.isNotEmpty; final hasAnyLogs = _scheduleLogs.isNotEmpty || _triennialLogs.isNotEmpty || _otherLogs.isNotEmpty;
final hasFilteredLogs = _filteredScheduleLogs.isNotEmpty || _filteredTriennialLogs.isNotEmpty || _filteredOtherLogs.isNotEmpty; final hasFilteredLogs = _filteredScheduleLogs.isNotEmpty || _filteredTriennialLogs.isNotEmpty || _filteredOtherLogs.isNotEmpty;
final logCategories = {
'Schedule': _filteredScheduleLogs,
'Triennial': _filteredTriennialLogs,
'Others': _filteredOtherLogs,
};
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('River Data Status Log')), appBar: AppBar(title: const Text('River Data Status Log')),
body: _isLoading body: _isLoading
@ -194,13 +219,9 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
: ListView( : ListView(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
children: [ children: [
// No global search bar here ...logCategories.entries
if (_scheduleLogs.isNotEmpty) .where((entry) => entry.value.isNotEmpty)
_buildCategorySection('Schedule', _filteredScheduleLogs), .map((entry) => _buildCategorySection(entry.key, entry.value)),
if (_triennialLogs.isNotEmpty)
_buildCategorySection('Triennial', _filteredTriennialLogs),
if (_otherLogs.isNotEmpty)
_buildCategorySection('Others', _filteredOtherLogs),
if (!hasFilteredLogs && hasAnyLogs) if (!hasFilteredLogs && hasAnyLogs)
const Center( const Center(
child: Padding( child: Padding(
@ -215,8 +236,6 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
} }
Widget _buildCategorySection(String category, List<SubmissionLogEntry> logs) { 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; final listHeight = (logs.length > 5 ? 5.5 : logs.length.toDouble()) * 75.0;
return Card( return Card(
@ -262,6 +281,8 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
Widget _buildLogListItem(SubmissionLogEntry log) { Widget _buildLogListItem(SubmissionLogEntry log) {
final isFailed = log.status != 'L3'; final isFailed = log.status != 'L3';
final logKey = log.reportId ?? log.submissionDateTime.toIso8601String();
final isResubmitting = _isResubmitting[logKey] ?? false;
final title = '${log.title} (${log.stationCode})'; final title = '${log.title} (${log.stationCode})';
final subtitle = DateFormat('yyyy-MM-dd HH:mm').format(log.submissionDateTime); 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)), title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(subtitle), subtitle: Text(subtitle),
trailing: isFailed trailing: isFailed
? (log.isResubmitting ? (isResubmitting
? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 3)) ? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 3))
: IconButton(icon: const Icon(Icons.sync, color: Colors.blue), tooltip: 'Resubmit', onPressed: () => _resubmitData(log))) : IconButton(icon: const Icon(Icons.sync, color: Colors.blue), tooltip: 'Resubmit', onPressed: () => _resubmitData(log)))
: null, : null,

View File

@ -25,7 +25,8 @@ class RiverApiService {
required Map<String, String> formData, required Map<String, String> formData,
required Map<String, File?> imageFiles, required Map<String, File?> imageFiles,
}) async { }) 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); final dataResult = await _baseService.post('river/manual/sample', formData);
if (dataResult['success'] != true) { if (dataResult['success'] != true) {
@ -64,8 +65,8 @@ class RiverApiService {
} }
final imageResult = await _baseService.postMultipart( final imageResult = await _baseService.postMultipart(
endpoint: 'river/manual/images', endpoint: 'river/manual/images', // Separate endpoint for images
fields: {'r_man_id': recordId.toString()}, fields: {'r_man_id': recordId.toString()}, // Link images to the submitted record ID
files: filesToUpload, files: filesToUpload,
); );
@ -111,7 +112,6 @@ class RiverApiService {
..writeln('*Status of Submission:* Successful'); ..writeln('*Status of Submission:* Successful');
if (distanceKm > 0 || (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A')) { if (distanceKm > 0 || (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A')) {
// CORRECTED: The cascade operator '..' was missing on the next line.
buffer buffer
..writeln() ..writeln()
..writeln('🔔 *Alert:*') ..writeln('🔔 *Alert:*')