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';
|
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,
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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: [
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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:*')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user