repair marine report log menu
This commit is contained in:
parent
da892821a2
commit
fa5f0361de
@ -106,6 +106,10 @@ import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_
|
||||
as marineManualEquipmentMaintenance;
|
||||
import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_data_status_log.dart'
|
||||
as marineManualDataStatusLog;
|
||||
// *** START: ADDED NEW IMPORT ***
|
||||
import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_report_status_log.dart'
|
||||
as marineManualReportStatusLog;
|
||||
// *** END: ADDED NEW IMPORT ***
|
||||
import 'package:environment_monitoring_app/screens/marine/manual/marine_image_request.dart' as marineManualImageRequest;
|
||||
import 'package:environment_monitoring_app/screens/marine/continuous/marine_continuous_info_centre_document.dart';
|
||||
import 'package:environment_monitoring_app/screens/marine/continuous/overview.dart' as marineContinuousOverview;
|
||||
@ -461,6 +465,10 @@ class _RootAppState extends State<RootApp> {
|
||||
'/marine/manual/report/maintenance': (context) =>
|
||||
const marineManualEquipmentMaintenance.MarineManualEquipmentMaintenanceScreen(),
|
||||
'/marine/manual/image-request': (context) => const marineManualImageRequest.MarineImageRequestScreen(),
|
||||
// *** START: ADDED NEW ROUTE ***
|
||||
'/marine/manual/report-log': (context) =>
|
||||
const marineManualReportStatusLog.MarineManualReportStatusLog(),
|
||||
// *** END: ADDED NEW ROUTE ***
|
||||
|
||||
// Marine Continuous
|
||||
'/marine/continuous/info': (context) => const MarineContinuousInfoCentreDocument(),
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
// lib/models/marine_manual_equipment_maintenance_data.dart
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
class MarineManualEquipmentMaintenanceData {
|
||||
int? conductedByUserId;
|
||||
// --- START: ADDED FIELD ---
|
||||
String? conductedByUserName;
|
||||
// --- END: ADDED FIELD ---
|
||||
String? maintenanceDate;
|
||||
String? lastMaintenanceDate;
|
||||
String? scheduleMaintenance;
|
||||
@ -25,6 +30,12 @@ class MarineManualEquipmentMaintenanceData {
|
||||
String? vanDornNewSerial;
|
||||
Map<String, Map<String, String>> vanDornReplacements = {};
|
||||
|
||||
// --- START: Added Fields ---
|
||||
String? submissionStatus;
|
||||
String? submissionMessage;
|
||||
String? reportId;
|
||||
// --- END: Added Fields ---
|
||||
|
||||
|
||||
// Constructor to initialize maps
|
||||
MarineManualEquipmentMaintenanceData() {
|
||||
@ -65,6 +76,36 @@ class MarineManualEquipmentMaintenanceData {
|
||||
vanDornReplacements[item] = {'Last Date': '', 'New Date': ''});
|
||||
}
|
||||
|
||||
// --- START: ADDED METHOD ---
|
||||
/// Creates a JSON object for offline database storage.
|
||||
Map<String, dynamic> toDbJson() {
|
||||
return {
|
||||
'conductedByUserId': conductedByUserId,
|
||||
'conductedByUserName': conductedByUserName,
|
||||
'maintenanceDate': maintenanceDate,
|
||||
'lastMaintenanceDate': lastMaintenanceDate,
|
||||
'scheduleMaintenance': scheduleMaintenance,
|
||||
'isReplacement': isReplacement,
|
||||
'timeStart': timeStart,
|
||||
'timeEnd': timeEnd,
|
||||
'location': location,
|
||||
'ysiSondeChecks': ysiSondeChecks,
|
||||
'ysiSondeComments': ysiSondeComments,
|
||||
'ysiSensorChecks': ysiSensorChecks,
|
||||
'ysiSensorComments': ysiSensorComments,
|
||||
'ysiReplacements': ysiReplacements,
|
||||
'vanDornChecks': vanDornChecks,
|
||||
'vanDornComments': vanDornComments,
|
||||
'vanDornCurrentSerial': vanDornCurrentSerial,
|
||||
'vanDornNewSerial': vanDornNewSerial,
|
||||
'vanDornReplacements': vanDornReplacements,
|
||||
'submissionStatus': submissionStatus,
|
||||
'submissionMessage': submissionMessage,
|
||||
'reportId': reportId,
|
||||
};
|
||||
}
|
||||
// --- END: ADDED METHOD ---
|
||||
|
||||
// MODIFIED: This method now builds the complex nested structure the PHP controller expects.
|
||||
Map<String, dynamic> toApiFormData() {
|
||||
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
// lib/models/marine_manual_pre_departure_checklist_data.dart
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
class MarineManualPreDepartureChecklistData {
|
||||
String? reporterName;
|
||||
int? reporterUserId;
|
||||
String? submissionDate;
|
||||
// --- START: ADDED FIELD ---
|
||||
String? location;
|
||||
// --- END: ADDED FIELD ---
|
||||
|
||||
// Key: Item description, Value: true if 'Yes', false if 'No'
|
||||
Map<String, bool> checklistItems = {};
|
||||
@ -11,8 +16,31 @@ class MarineManualPreDepartureChecklistData {
|
||||
// Key: Item description, Value: Remarks text
|
||||
Map<String, String> remarks = {};
|
||||
|
||||
// --- START: Added Fields ---
|
||||
String? submissionStatus;
|
||||
String? submissionMessage;
|
||||
String? reportId;
|
||||
// --- END: Added Fields ---
|
||||
|
||||
MarineManualPreDepartureChecklistData();
|
||||
|
||||
// --- START: ADDED METHOD ---
|
||||
/// Creates a JSON object for offline database storage.
|
||||
Map<String, dynamic> toDbJson() {
|
||||
return {
|
||||
'reporterName': reporterName,
|
||||
'reporterUserId': reporterUserId,
|
||||
'submissionDate': submissionDate,
|
||||
'location': location, // <-- ADDED
|
||||
'checklistItems': checklistItems,
|
||||
'remarks': remarks,
|
||||
'submissionStatus': submissionStatus,
|
||||
'submissionMessage': submissionMessage,
|
||||
'reportId': reportId,
|
||||
};
|
||||
}
|
||||
// --- END: ADDED METHOD ---
|
||||
|
||||
// MODIFIED: This method now builds the nested array structure the PHP controller expects.
|
||||
Map<String, dynamic> toApiFormData() {
|
||||
|
||||
@ -31,6 +59,9 @@ class MarineManualPreDepartureChecklistData {
|
||||
return {
|
||||
'reporter_user_id': reporterUserId.toString(), // The controller gets this from auth, but good to send.
|
||||
'submission_date': submissionDate,
|
||||
// Note: 'location' is not sent to the API in this method,
|
||||
// but it will be saved in the local log via toDbJson().
|
||||
// If the API needs it, it must be added here.
|
||||
'items': itemsList, // Send the formatted list
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
// lib/models/marine_manual_sonde_calibration_data.dart
|
||||
|
||||
class MarineManualSondeCalibrationData {
|
||||
int? calibratedByUserId;
|
||||
// --- START: ADDED FIELD ---
|
||||
String? calibratedByUserName;
|
||||
// --- END: ADDED FIELD ---
|
||||
|
||||
// Header fields from PDF
|
||||
String? sondeSerialNumber;
|
||||
@ -30,6 +35,12 @@ class MarineManualSondeCalibrationData {
|
||||
String? calibrationStatus;
|
||||
String? remarks; // Matches "COMMENT/OBSERVATION"
|
||||
|
||||
// --- START: Added Fields ---
|
||||
String? submissionStatus;
|
||||
String? submissionMessage;
|
||||
String? reportId;
|
||||
// --- END: Added Fields ---
|
||||
|
||||
Map<String, dynamic> toApiFormData() {
|
||||
// This flat structure matches MarineSondeCalibrationController.php
|
||||
return {
|
||||
@ -58,4 +69,39 @@ class MarineManualSondeCalibrationData {
|
||||
'remarks': remarks,
|
||||
};
|
||||
}
|
||||
|
||||
// --- START: ADDED toDbJson METHOD ---
|
||||
/// Creates a JSON object for offline database storage.
|
||||
Map<String, dynamic> toDbJson() {
|
||||
return {
|
||||
'calibratedByUserId': calibratedByUserId,
|
||||
'calibratedByUserName': calibratedByUserName, // <-- ADDED
|
||||
'sondeSerialNumber': sondeSerialNumber,
|
||||
'firmwareVersion': firmwareVersion,
|
||||
'korVersion': korVersion,
|
||||
'location': location,
|
||||
'startDateTime': startDateTime,
|
||||
'endDateTime': endDateTime,
|
||||
'ph_7_mv': ph7Mv,
|
||||
'ph_7_before': ph7Before,
|
||||
'ph_7_after': ph7After,
|
||||
'ph_10_mv': ph10Mv,
|
||||
'ph_10_before': ph10Before,
|
||||
'ph_10_after': ph10After,
|
||||
'cond_before': condBefore,
|
||||
'cond_after': condAfter,
|
||||
'do_before': doBefore,
|
||||
'do_after': doAfter,
|
||||
'turbidity_0_before': turbidity0Before,
|
||||
'turbidity_0_after': turbidity0After,
|
||||
'turbidity_124_before': turbidity124Before,
|
||||
'turbidity_124_after': turbidity124After,
|
||||
'calibration_status': calibrationStatus,
|
||||
'remarks': remarks,
|
||||
'submissionStatus': submissionStatus,
|
||||
'submissionMessage': submissionMessage,
|
||||
'reportId': reportId,
|
||||
};
|
||||
}
|
||||
// --- END: ADDED toDbJson METHOD ---
|
||||
}
|
||||
@ -68,21 +68,13 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
late MarineInSituSamplingService _marineInSituService;
|
||||
late MarineTarballSamplingService _marineTarballService;
|
||||
|
||||
// --- START: MODIFIED STATE FOR DROPDOWN ---
|
||||
String _selectedModule = 'Manual Sampling';
|
||||
final List<String> _modules = ['Manual Sampling', 'Tarball Sampling', 'Pre-Sampling', 'Report'];
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
List<SubmissionLogEntry> _manualLogs = [];
|
||||
List<SubmissionLogEntry> _tarballLogs = [];
|
||||
List<SubmissionLogEntry> _preSamplingLogs = []; // No data source, will be empty
|
||||
List<SubmissionLogEntry> _reportLogs = []; // Will hold NPE logs
|
||||
|
||||
List<SubmissionLogEntry> _filteredManualLogs = [];
|
||||
List<SubmissionLogEntry> _filteredTarballLogs = [];
|
||||
List<SubmissionLogEntry> _filteredPreSamplingLogs = [];
|
||||
List<SubmissionLogEntry> _filteredReportLogs = [];
|
||||
// --- END: MODIFIED STATE ---
|
||||
|
||||
final TextEditingController _manualSearchController = TextEditingController();
|
||||
final TextEditingController _tarballSearchController = TextEditingController();
|
||||
|
||||
bool _isLoading = true;
|
||||
final Map<String, bool> _isResubmitting = {};
|
||||
@ -92,7 +84,8 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
super.initState();
|
||||
// MODIFIED: Service instantiations are removed from initState.
|
||||
// They will be initialized in didChangeDependencies.
|
||||
_searchController.addListener(_filterLogs); // Use single search controller
|
||||
_manualSearchController.addListener(_filterLogs);
|
||||
_tarballSearchController.addListener(_filterLogs);
|
||||
_loadAllLogs();
|
||||
}
|
||||
|
||||
@ -102,46 +95,45 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
// Fetch the single, global instances of the services from the Provider tree.
|
||||
// --- START FIX: Added listen: false to prevent unnecessary rebuilds ---
|
||||
_marineInSituService = Provider.of<MarineInSituSamplingService>(context, listen: false);
|
||||
_marineTarballService = Provider.of<MarineTarballSamplingService>(context, listen: false);
|
||||
// --- END FIX ---
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose(); // Dispose single search controller
|
||||
_manualSearchController.dispose();
|
||||
_tarballSearchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadAllLogs() async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
// --- START MODIFICATION: Load logs sequentially to avoid parallel permission requests ---
|
||||
// final [tarballLogs, inSituLogs, npeLogs] = await Future.wait([
|
||||
// _localStorageService.getAllTarballLogs(),
|
||||
// _localStorageService.getAllInSituLogs(),
|
||||
// _localStorageService.getAllNpeLogs(), // Fetch NPE logs for "Report"
|
||||
// ]);
|
||||
final tarballLogs = await _localStorageService.getAllTarballLogs();
|
||||
final inSituLogs = await _localStorageService.getAllInSituLogs();
|
||||
final npeLogs = await _localStorageService.getAllNpeLogs();
|
||||
// --- END MODIFICATION ---
|
||||
|
||||
final List<SubmissionLogEntry> tempManual = [];
|
||||
final List<SubmissionLogEntry> tempTarball = [];
|
||||
final List<SubmissionLogEntry> tempReport = [];
|
||||
final List<SubmissionLogEntry> tempPreSampling = []; // Empty list
|
||||
|
||||
// Process In-Situ (Manual Sampling)
|
||||
for (var log in inSituLogs) {
|
||||
// START FIX: Use backward-compatible keys to read the timestamp
|
||||
final String dateStr = log['sampling_date'] ?? log['man_date'] ?? '';
|
||||
final String timeStr = log['sampling_time'] ?? log['man_time'] ?? '';
|
||||
// END FIX
|
||||
|
||||
// --- START FIX: Prevent fallback to DateTime.now() to make errors visible ---
|
||||
final dt = DateTime.tryParse('$dateStr $timeStr');
|
||||
// --- END FIX ---
|
||||
|
||||
tempManual.add(SubmissionLogEntry(
|
||||
type: 'Manual Sampling',
|
||||
title: log['selectedStation']?['man_station_name'] ?? 'Unknown Station',
|
||||
stationCode: log['selectedStation']?['man_station_code'] ?? 'N/A',
|
||||
// --- START FIX: Use the parsed date or a placeholder for invalid entries ---
|
||||
submissionDateTime: dt ?? DateTime.fromMillisecondsSinceEpoch(0),
|
||||
// --- END FIX ---
|
||||
reportId: log['reportId']?.toString(),
|
||||
status: log['submissionStatus'] ?? 'L1',
|
||||
message: log['submissionMessage'] ?? 'No status message.',
|
||||
@ -152,17 +144,15 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
));
|
||||
}
|
||||
|
||||
// Process Tarball
|
||||
for (var log in tarballLogs) {
|
||||
final dateStr = log['sampling_date'] ?? '';
|
||||
final timeStr = log['sampling_time'] ?? '';
|
||||
final dt = DateTime.tryParse('$dateStr $timeStr');
|
||||
|
||||
tempTarball.add(SubmissionLogEntry(
|
||||
type: 'Tarball Sampling',
|
||||
title: log['selectedStation']?['tbl_station_name'] ?? 'Unknown Station',
|
||||
stationCode: log['selectedStation']?['tbl_station_code'] ?? 'N/A',
|
||||
submissionDateTime: dt ?? DateTime.fromMillisecondsSinceEpoch(0),
|
||||
submissionDateTime: DateTime.tryParse('$dateStr $timeStr') ?? DateTime.fromMillisecondsSinceEpoch(0),
|
||||
reportId: log['reportId']?.toString(),
|
||||
status: log['submissionStatus'] ?? 'L1',
|
||||
message: log['submissionMessage'] ?? 'No status message.',
|
||||
@ -173,74 +163,28 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
));
|
||||
}
|
||||
|
||||
// --- START: ADDED NPE LOG PROCESSING ---
|
||||
// Process NPE (Report)
|
||||
for (var log in npeLogs) {
|
||||
final dateStr = log['eventDate'] ?? '';
|
||||
final timeStr = log['eventTime'] ?? '';
|
||||
final dt = DateTime.tryParse('$dateStr $timeStr');
|
||||
|
||||
String stationName = 'N/A';
|
||||
String stationCode = 'N/A';
|
||||
if (log['selectedStation'] != null) {
|
||||
stationName = log['selectedStation']['man_station_name'] ??
|
||||
log['selectedStation']['tbl_station_name'] ??
|
||||
'Unknown Station';
|
||||
stationCode = log['selectedStation']['man_station_code'] ??
|
||||
log['selectedStation']['tbl_station_code'] ??
|
||||
'N/A';
|
||||
} else if (log['locationDescription'] != null) {
|
||||
stationName = log['locationDescription'];
|
||||
stationCode = 'New Location';
|
||||
}
|
||||
|
||||
tempReport.add(SubmissionLogEntry(
|
||||
type: 'NPE Report',
|
||||
title: stationName,
|
||||
stationCode: stationCode,
|
||||
submissionDateTime: dt ?? DateTime.fromMillisecondsSinceEpoch(0),
|
||||
reportId: log['reportId']?.toString(),
|
||||
status: log['submissionStatus'] ?? 'L1',
|
||||
message: log['submissionMessage'] ?? 'No status message.',
|
||||
rawData: log,
|
||||
serverName: log['serverConfigName'] ?? 'Unknown Server',
|
||||
apiStatusRaw: log['api_status'],
|
||||
ftpStatusRaw: log['ftp_status'],
|
||||
));
|
||||
}
|
||||
// --- END: ADDED NPE LOG PROCESSING ---
|
||||
|
||||
// Sort all lists
|
||||
tempManual.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
|
||||
tempTarball.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
|
||||
tempReport.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_manualLogs = tempManual;
|
||||
_tarballLogs = tempTarball;
|
||||
_reportLogs = tempReport;
|
||||
_preSamplingLogs = tempPreSampling; // Stays empty
|
||||
_isLoading = false;
|
||||
});
|
||||
_filterLogs(); // Apply initial filter
|
||||
_filterLogs();
|
||||
}
|
||||
}
|
||||
|
||||
// --- START: MODIFIED _filterLogs ---
|
||||
void _filterLogs() {
|
||||
final query = _searchController.text.toLowerCase();
|
||||
final manualQuery = _manualSearchController.text.toLowerCase();
|
||||
final tarballQuery = _tarballSearchController.text.toLowerCase();
|
||||
|
||||
setState(() {
|
||||
// We filter all lists regardless of selection, so data is ready
|
||||
// if the user switches modules.
|
||||
_filteredManualLogs = _manualLogs.where((log) => _logMatchesQuery(log, query)).toList();
|
||||
_filteredTarballLogs = _tarballLogs.where((log) => _logMatchesQuery(log, query)).toList();
|
||||
_filteredPreSamplingLogs = _preSamplingLogs.where((log) => _logMatchesQuery(log, query)).toList();
|
||||
_filteredReportLogs = _reportLogs.where((log) => _logMatchesQuery(log, query)).toList();
|
||||
_filteredManualLogs = _manualLogs.where((log) => _logMatchesQuery(log, manualQuery)).toList();
|
||||
_filteredTarballLogs = _tarballLogs.where((log) => _logMatchesQuery(log, tarballQuery)).toList();
|
||||
});
|
||||
}
|
||||
// --- END: MODIFIED _filterLogs ---
|
||||
|
||||
bool _logMatchesQuery(SubmissionLogEntry log, String query) {
|
||||
if (query.isEmpty) return true;
|
||||
@ -322,13 +266,6 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
context: context,
|
||||
);
|
||||
}
|
||||
// --- ADD RESUBMIT FOR NPE ---
|
||||
else if (log.type == 'NPE Report') {
|
||||
// As of now, resubmission for NPE is not defined in the provided files.
|
||||
// We will show a message and not attempt resubmission.
|
||||
result = {'message': 'Resubmission for NPE Reports is not currently supported.'};
|
||||
}
|
||||
// --- END ADD ---
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@ -351,151 +288,75 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
}
|
||||
}
|
||||
|
||||
// --- START: MODIFIED build ---
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasAnyLogs = _manualLogs.isNotEmpty || _tarballLogs.isNotEmpty;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Marine Manual Data Status Log')),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
: RefreshIndicator(
|
||||
onRefresh: _loadAllLogs,
|
||||
child: !hasAnyLogs
|
||||
? const Center(child: Text('No submission logs found.'))
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// --- WIDGET 1: DROPDOWN ---
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedModule,
|
||||
items: _modules.map((module) {
|
||||
return DropdownMenuItem(value: module, child: Text(module));
|
||||
}).toList(),
|
||||
onChanged: (newValue) {
|
||||
if (newValue != null) {
|
||||
setState(() {
|
||||
_selectedModule = newValue;
|
||||
_searchController.clear(); // Clear search on module change
|
||||
});
|
||||
_filterLogs(); // Apply filter for new module
|
||||
_buildCategorySection('Manual Sampling', _filteredManualLogs, _manualSearchController),
|
||||
_buildCategorySection('Tarball Sampling', _filteredTarballLogs, _tarballSearchController),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Select Module',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// --- WIDGET 2: THE "BLACK BOX" CARD ---
|
||||
Expanded(
|
||||
child: Card(
|
||||
// Use Card's color or specify black-ish if needed
|
||||
// color: Theme.of(context).cardColor,
|
||||
elevation: 2,
|
||||
Widget _buildCategorySection(String category, List<SubmissionLogEntry> logs, TextEditingController searchController) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// --- SEARCH BAR (MOVED INSIDE CARD) ---
|
||||
Padding(
|
||||
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: _searchController,
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search in $_selectedModule...',
|
||||
hintText: 'Search in $category...',
|
||||
prefixIcon: const Icon(Icons.search, size: 20),
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: _searchController.text.isNotEmpty ? IconButton(
|
||||
suffixIcon: searchController.text.isNotEmpty ? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
searchController.clear();
|
||||
},
|
||||
) : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
|
||||
// --- LOG LIST (MOVED INSIDE CARD) ---
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _loadAllLogs,
|
||||
child: _buildCurrentModuleList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// --- END: MODIFIED build ---
|
||||
|
||||
// --- START: NEW WIDGET _buildCurrentModuleList ---
|
||||
/// Builds the list view based on the currently selected dropdown module.
|
||||
Widget _buildCurrentModuleList() {
|
||||
switch (_selectedModule) {
|
||||
case 'Manual Sampling':
|
||||
return _buildLogList(
|
||||
filteredLogs: _filteredManualLogs,
|
||||
totalLogCount: _manualLogs.length,
|
||||
);
|
||||
case 'Tarball Sampling':
|
||||
return _buildLogList(
|
||||
filteredLogs: _filteredTarballLogs,
|
||||
totalLogCount: _tarballLogs.length,
|
||||
);
|
||||
case 'Pre-Sampling':
|
||||
return _buildLogList(
|
||||
filteredLogs: _filteredPreSamplingLogs,
|
||||
totalLogCount: _preSamplingLogs.length,
|
||||
);
|
||||
case 'Report':
|
||||
return _buildLogList(
|
||||
filteredLogs: _filteredReportLogs,
|
||||
totalLogCount: _reportLogs.length,
|
||||
);
|
||||
default:
|
||||
return const Center(child: Text('Please select a module.'));
|
||||
}
|
||||
}
|
||||
// --- END: NEW WIDGET _buildCurrentModuleList ---
|
||||
|
||||
// --- START: MODIFIED WIDGET _buildLogList ---
|
||||
/// A generic list builder that simply shows all filtered logs in a scrollable list.
|
||||
Widget _buildLogList({
|
||||
required List<SubmissionLogEntry> filteredLogs,
|
||||
required int totalLogCount,
|
||||
}) {
|
||||
if (filteredLogs.isEmpty) {
|
||||
final String message = _searchController.text.isNotEmpty
|
||||
? 'No logs match your search.'
|
||||
: (totalLogCount == 0 ? 'No submission logs found.' : 'No logs match your search.');
|
||||
|
||||
// Use a ListView to allow RefreshIndicator to work even when empty
|
||||
return ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Center(child: Text(message)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Standard ListView.builder renders all filtered logs.
|
||||
// It inherently only builds visible items, so it is efficient.
|
||||
return ListView.builder(
|
||||
itemCount: filteredLogs.length,
|
||||
const Divider(),
|
||||
if (logs.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(child: Text('No logs match your search in this category.')))
|
||||
else
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: logs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final log = filteredLogs[index];
|
||||
return _buildLogListItem(log);
|
||||
return _buildLogListItem(logs[index]);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// --- END: MODIFIED WIDGET _buildLogList ---
|
||||
|
||||
Widget _buildLogListItem(SubmissionLogEntry log) {
|
||||
final logKey = log.reportId ?? log.submissionDateTime.toIso8601String();
|
||||
@ -505,9 +366,9 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
// Define the different states based on the detailed status code.
|
||||
final bool isFullSuccess = log.status == 'S4';
|
||||
final bool isPartialSuccess = log.status == 'S3' || log.status == 'L4';
|
||||
final bool canResubmit = !isFullSuccess && log.type != 'NPE Report'; // --- MODIFIED: Disable resubmit for NPE
|
||||
// --- END: MODIFICATION FOR GRANULAR STATUS ICONS ---
|
||||
final bool canResubmit = !isFullSuccess; // Allow resubmission for partial success or failure.
|
||||
|
||||
// Determine the icon and color based on the state.
|
||||
IconData statusIcon;
|
||||
Color statusColor;
|
||||
|
||||
@ -521,6 +382,7 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
statusIcon = Icons.error_outline;
|
||||
statusColor = Colors.red;
|
||||
}
|
||||
// --- END: MODIFICATION FOR GRANULAR STATUS ICONS ---
|
||||
|
||||
final titleWidget = RichText(
|
||||
text: TextSpan(
|
||||
@ -561,7 +423,7 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
_buildDetailRow('Report ID:', log.reportId ?? 'N/A'),
|
||||
_buildDetailRow('Submission Type:', log.type),
|
||||
|
||||
// --- START: ADDED BUTTONS ---
|
||||
// --- START: ADDED BUTTONS AND GRANULAR STATUS ---
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
@ -580,11 +442,10 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// --- END: ADDED BUTTONS ---
|
||||
|
||||
const Divider(height: 10), // --- ADDED DIVIDER ---
|
||||
_buildGranularStatus('API', log.apiStatusRaw), // --- ADDED ---
|
||||
_buildGranularStatus('FTP', log.ftpStatusRaw), // --- ADDED ---
|
||||
const Divider(height: 10),
|
||||
_buildGranularStatus('API', log.apiStatusRaw),
|
||||
_buildGranularStatus('FTP', log.ftpStatusRaw),
|
||||
// --- END: ADDED BUTTONS AND GRANULAR STATUS ---
|
||||
],
|
||||
),
|
||||
)
|
||||
@ -592,7 +453,23 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
);
|
||||
}
|
||||
|
||||
// --- START: NEW HELPER WIDGETS FOR CATEGORIZED DIALOG ---
|
||||
Widget _buildDetailRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(flex: 2, child: Text(label, style: const TextStyle(fontWeight: FontWeight.bold))),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(flex: 3, child: Text(value)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// --- START: ADDED METHODS FOR VIEW DATA / VIEW IMAGE FUNCTIONALITY ---
|
||||
// =========================================================================
|
||||
|
||||
/// Builds a formatted category header row for the data table.
|
||||
TableRow _buildCategoryRow(BuildContext context, String title, IconData icon) {
|
||||
@ -611,7 +488,7 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
@ -636,11 +513,24 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||
child: Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
// --- START: MODIFICATION FOR FONT SIZE ---
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11.0, // <-- ADJUST THIS VALUE AS NEEDED
|
||||
),
|
||||
),
|
||||
// --- END: MODIFICATION FOR FONT SIZE ---
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||
child: Text(displayValue), // Use Text, NOT SelectableText
|
||||
// --- START: MODIFICATION FOR FONT SIZE ---
|
||||
child: Text(
|
||||
displayValue,
|
||||
style: const TextStyle(fontSize: 11.0), // <-- ADJUST THIS VALUE AS NEEDED
|
||||
),
|
||||
// --- END: MODIFICATION FOR FONT SIZE ---
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -653,11 +543,6 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
if (value is double && value == -999.0) return 'N/A';
|
||||
return value.toString();
|
||||
}
|
||||
// --- END: NEW HELPER WIDGETS ---
|
||||
|
||||
// =========================================================================
|
||||
// --- START OF MODIFIED FUNCTION ---
|
||||
// =========================================================================
|
||||
|
||||
/// Shows the categorized and formatted data log in a dialog
|
||||
void _showDataDialog(BuildContext context, SubmissionLogEntry log) {
|
||||
@ -666,7 +551,6 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
|
||||
// --- 1. Sampling Info ---
|
||||
tableRows.add(_buildCategoryRow(context, 'Sampling Info', Icons.calendar_today));
|
||||
// --- MODIFIED: Added keys for NPE Report
|
||||
tableRows.add(_buildDataTableRow('Date', _getString(data, 'sampling_date') ?? _getString(data, 'man_date') ?? _getString(data, 'eventDate')));
|
||||
tableRows.add(_buildDataTableRow('Time', _getString(data, 'sampling_time') ?? _getString(data, 'man_time') ?? _getString(data, 'eventTime')));
|
||||
|
||||
@ -694,7 +578,6 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
tableRows.add(_buildDataTableRow('Station Latitude', stationLat));
|
||||
tableRows.add(_buildDataTableRow('Station Longitude', stationLon));
|
||||
|
||||
// --- MODIFIED: Added 'latitude'/'longitude' keys for NPE
|
||||
tableRows.add(_buildDataTableRow('Current Latitude', _getString(data, 'current_latitude') ?? _getString(data, 'currentLatitude') ?? _getString(data, 'latitude')));
|
||||
tableRows.add(_buildDataTableRow('Current Longitude', _getString(data, 'current_longitude') ?? _getString(data, 'currentLongitude') ?? _getString(data, 'longitude')));
|
||||
|
||||
@ -708,7 +591,6 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
tableRows.add(_buildDataTableRow('Sea', _getString(data, 'sea_condition') ?? _getString(data, 'seaCondition') ?? _getString(data, 'sea_condition_manual')));
|
||||
tableRows.add(_buildDataTableRow('Weather', _getString(data, 'weather') ?? _getString(data, 'weather_manual')));
|
||||
|
||||
// --- MODIFIED: Use correct plural keys first
|
||||
tableRows.add(_buildDataTableRow('Event Remarks', _getString(data, 'event_remarks') ?? _getString(data, 'man_event_remark') ?? _getString(data, 'eventRemark')));
|
||||
tableRows.add(_buildDataTableRow('Lab Remarks', _getString(data, 'lab_remarks') ?? _getString(data, 'man_lab_remark') ?? _getString(data, 'labRemark')));
|
||||
}
|
||||
@ -790,14 +672,9 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// --- END OF MODIFIED FUNCTION ---
|
||||
// =========================================================================
|
||||
|
||||
/// Shows the image gallery dialog
|
||||
void _showImageDialog(BuildContext context, SubmissionLogEntry log) {
|
||||
|
||||
// --- START: MODIFIED to handle all log types ---
|
||||
final List<ImageLogEntry> imageEntries = [];
|
||||
|
||||
if (log.type == 'Manual Sampling') {
|
||||
@ -839,7 +716,6 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
};
|
||||
_addImagesToList(log, imageRemarkMap, imageEntries);
|
||||
}
|
||||
// --- END: MODIFIED ---
|
||||
|
||||
|
||||
if (imageEntries.isEmpty) {
|
||||
@ -952,7 +828,6 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- END: NEW HELPER ---
|
||||
|
||||
Widget _buildGranularStatus(String type, String? jsonStatus) {
|
||||
if (jsonStatus == null || jsonStatus.isEmpty) {
|
||||
@ -999,18 +874,7 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(flex: 2, child: Text(label, style: const TextStyle(fontWeight: FontWeight.bold))),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(flex: 3, child: Text(value)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// =========================================================================
|
||||
// --- END: ADDED METHODS FOR VIEW DATA / VIEW IMAGE FUNCTIONALITY ---
|
||||
// =========================================================================
|
||||
}
|
||||
1291
lib/screens/marine/manual/marine_manual_report_status_log.dart
Normal file
1291
lib/screens/marine/manual/marine_manual_report_status_log.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -41,6 +41,9 @@ class MarineHomePage extends StatelessWidget {
|
||||
SidebarItem(icon: Icons.article, label: "Data Log", route: '/marine/manual/data-log'),
|
||||
SidebarItem(icon: Icons.image, label: "Image Request", route: '/marine/manual/image-request'),
|
||||
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/manual/report'),
|
||||
// *** START: ADDED NEW MENU ITEM ***
|
||||
SidebarItem(icon: Icons.history_edu_outlined, label: "Report Status Log", route: '/marine/manual/report-log'),
|
||||
// *** END: ADDED NEW MENU ITEM ***
|
||||
],
|
||||
),
|
||||
SidebarItem(
|
||||
|
||||
@ -14,6 +14,11 @@ import '../models/air_collection_data.dart';
|
||||
import '../models/tarball_data.dart';
|
||||
import '../models/in_situ_sampling_data.dart';
|
||||
import '../models/marine_manual_npe_report_data.dart';
|
||||
// --- ADDED: Imports for new data models ---
|
||||
import '../models/marine_manual_pre_departure_checklist_data.dart';
|
||||
import '../models/marine_manual_sonde_calibration_data.dart';
|
||||
import '../models/marine_manual_equipment_maintenance_data.dart';
|
||||
// --- END ADDED ---
|
||||
import '../models/river_in_situ_sampling_data.dart';
|
||||
import '../models/river_manual_triennial_sampling_data.dart';
|
||||
// --- ADDED IMPORT ---
|
||||
@ -511,7 +516,7 @@ class LocalStorageService {
|
||||
try {
|
||||
final String originalFileName = p.basename(imageFile.path);
|
||||
final File newFile = await imageFile.copy(p.join(eventDir.path, originalFileName));
|
||||
jsonData[entry.key] = newFile.path;
|
||||
jsonData['image${entry.key.split('_').last}Path'] = newFile.path; // Save as image1Path, etc.
|
||||
} catch (e) {
|
||||
debugPrint("Error processing NPE image file ${imageFile.path}: $e");
|
||||
}
|
||||
@ -576,6 +581,268 @@ class LocalStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// --- START: Added Part 4.6: Marine Pre-Departure Methods ---
|
||||
// =======================================================================
|
||||
|
||||
Future<Directory?> _getPreDepartureBaseDir({required String serverName}) async {
|
||||
final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName);
|
||||
if (mmsv4Dir == null) return null;
|
||||
|
||||
final logDir = Directory(p.join(mmsv4Dir.path, 'marine', 'marine_pre_departure'));
|
||||
if (!await logDir.exists()) {
|
||||
await logDir.create(recursive: true);
|
||||
}
|
||||
return logDir;
|
||||
}
|
||||
|
||||
Future<String?> savePreDepartureData(MarineManualPreDepartureChecklistData data, {required String serverName}) async {
|
||||
final baseDir = await _getPreDepartureBaseDir(serverName: serverName);
|
||||
if (baseDir == null) return null;
|
||||
|
||||
try {
|
||||
final timestamp = data.submissionDate ?? DateTime.now().toIso8601String();
|
||||
final eventFolderName = "checklist_$timestamp";
|
||||
final eventDir = Directory(p.join(baseDir.path, eventFolderName));
|
||||
if (!await eventDir.exists()) {
|
||||
await eventDir.create(recursive: true);
|
||||
}
|
||||
|
||||
// --- START: MODIFIED BLOCK ---
|
||||
// Use toDbJson() for a complete, consistent log
|
||||
final Map<String, dynamic> jsonData = data.toDbJson();
|
||||
jsonData['serverConfigName'] = serverName;
|
||||
// All other fields are now included in toDbJson()
|
||||
// --- END: MODIFIED BLOCK ---
|
||||
|
||||
final jsonFile = File(p.join(eventDir.path, 'data.json'));
|
||||
await jsonFile.writeAsString(jsonEncode(jsonData));
|
||||
debugPrint("Pre-Departure log saved to: ${jsonFile.path}");
|
||||
return eventDir.path;
|
||||
} catch (e) {
|
||||
debugPrint("Error saving Pre-Departure log to local storage: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getAllPreDepartureLogs() async {
|
||||
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
|
||||
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
|
||||
|
||||
final List<Map<String, dynamic>> allLogs = [];
|
||||
final serverDirs = mmsv4Root.listSync().whereType<Directory>();
|
||||
|
||||
for (var serverDir in serverDirs) {
|
||||
final baseDir = Directory(p.join(serverDir.path, 'marine', 'marine_pre_departure'));
|
||||
if (!await baseDir.exists()) continue;
|
||||
try {
|
||||
final entities = baseDir.listSync();
|
||||
for (var entity in entities) {
|
||||
if (entity is Directory) {
|
||||
final jsonFile = File(p.join(entity.path, 'data.json'));
|
||||
if (await jsonFile.exists()) {
|
||||
final data = jsonDecode(await jsonFile.readAsString()) as Map<String, dynamic>;
|
||||
data['logDirectory'] = entity.path;
|
||||
allLogs.add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error reading Pre-Departure logs from ${baseDir.path}: $e");
|
||||
}
|
||||
}
|
||||
return allLogs;
|
||||
}
|
||||
|
||||
Future<void> updatePreDepartureLog(Map<String, dynamic> updatedLogData) async {
|
||||
final logDir = updatedLogData['logDirectory'];
|
||||
if (logDir == null) return;
|
||||
try {
|
||||
final jsonFile = File(p.join(logDir, 'data.json'));
|
||||
if (await jsonFile.exists()) {
|
||||
updatedLogData.remove('isResubmitting');
|
||||
await jsonFile.writeAsString(jsonEncode(updatedLogData));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error updating Pre-Departure log: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// --- START: Added Part 4.7: Marine Sonde Calibration Methods ---
|
||||
// =======================================================================
|
||||
|
||||
Future<Directory?> _getSondeCalibrationBaseDir({required String serverName}) async {
|
||||
final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName);
|
||||
if (mmsv4Dir == null) return null;
|
||||
|
||||
final logDir = Directory(p.join(mmsv4Dir.path, 'marine', 'marine_sonde_calibration'));
|
||||
if (!await logDir.exists()) {
|
||||
await logDir.create(recursive: true);
|
||||
}
|
||||
return logDir;
|
||||
}
|
||||
|
||||
Future<String?> saveSondeCalibrationData(MarineManualSondeCalibrationData data, {required String serverName}) async {
|
||||
final baseDir = await _getSondeCalibrationBaseDir(serverName: serverName);
|
||||
if (baseDir == null) return null;
|
||||
|
||||
try {
|
||||
final timestamp = data.startDateTime?.replaceAll(':', '-').replaceAll(' ', '_') ?? DateTime.now().toIso8601String();
|
||||
final eventFolderName = "calibration_${data.sondeSerialNumber}_$timestamp";
|
||||
final eventDir = Directory(p.join(baseDir.path, eventFolderName));
|
||||
if (!await eventDir.exists()) {
|
||||
await eventDir.create(recursive: true);
|
||||
}
|
||||
|
||||
// --- START: MODIFIED BLOCK ---
|
||||
// Use toDbJson() for a complete, consistent log
|
||||
final Map<String, dynamic> jsonData = data.toDbJson();
|
||||
jsonData['serverConfigName'] = serverName;
|
||||
// All other fields are now included in toDbJson()
|
||||
// --- END: MODIFIED BLOCK ---
|
||||
|
||||
final jsonFile = File(p.join(eventDir.path, 'data.json'));
|
||||
await jsonFile.writeAsString(jsonEncode(jsonData));
|
||||
debugPrint("Sonde Calibration log saved to: ${jsonFile.path}");
|
||||
return eventDir.path;
|
||||
} catch (e) {
|
||||
debugPrint("Error saving Sonde Calibration log to local storage: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getAllSondeCalibrationLogs() async {
|
||||
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
|
||||
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
|
||||
|
||||
final List<Map<String, dynamic>> allLogs = [];
|
||||
final serverDirs = mmsv4Root.listSync().whereType<Directory>();
|
||||
|
||||
for (var serverDir in serverDirs) {
|
||||
final baseDir = Directory(p.join(serverDir.path, 'marine', 'marine_sonde_calibration'));
|
||||
if (!await baseDir.exists()) continue;
|
||||
try {
|
||||
final entities = baseDir.listSync();
|
||||
for (var entity in entities) {
|
||||
if (entity is Directory) {
|
||||
final jsonFile = File(p.join(entity.path, 'data.json'));
|
||||
if (await jsonFile.exists()) {
|
||||
final data = jsonDecode(await jsonFile.readAsString()) as Map<String, dynamic>;
|
||||
data['logDirectory'] = entity.path;
|
||||
allLogs.add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error reading Sonde Calibration logs from ${baseDir.path}: $e");
|
||||
}
|
||||
}
|
||||
return allLogs;
|
||||
}
|
||||
|
||||
Future<void> updateSondeCalibrationLog(Map<String, dynamic> updatedLogData) async {
|
||||
final logDir = updatedLogData['logDirectory'];
|
||||
if (logDir == null) return;
|
||||
try {
|
||||
final jsonFile = File(p.join(logDir, 'data.json'));
|
||||
if (await jsonFile.exists()) {
|
||||
updatedLogData.remove('isResubmitting');
|
||||
await jsonFile.writeAsString(jsonEncode(updatedLogData));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error updating Sonde Calibration log: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// --- START: Added Part 4.8: Marine Equipment Maintenance Methods ---
|
||||
// =======================================================================
|
||||
|
||||
Future<Directory?> _getEquipmentMaintenanceBaseDir({required String serverName}) async {
|
||||
final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName);
|
||||
if (mmsv4Dir == null) return null;
|
||||
|
||||
final logDir = Directory(p.join(mmsv4Dir.path, 'marine', 'marine_equipment_maintenance'));
|
||||
if (!await logDir.exists()) {
|
||||
await logDir.create(recursive: true);
|
||||
}
|
||||
return logDir;
|
||||
}
|
||||
|
||||
Future<String?> saveEquipmentMaintenanceData(MarineManualEquipmentMaintenanceData data, {required String serverName}) async {
|
||||
final baseDir = await _getEquipmentMaintenanceBaseDir(serverName: serverName);
|
||||
if (baseDir == null) return null;
|
||||
|
||||
try {
|
||||
final timestamp = data.maintenanceDate ?? DateTime.now().toIso8601String();
|
||||
final eventFolderName = "maintenance_$timestamp";
|
||||
final eventDir = Directory(p.join(baseDir.path, eventFolderName));
|
||||
if (!await eventDir.exists()) {
|
||||
await eventDir.create(recursive: true);
|
||||
}
|
||||
|
||||
// --- START: MODIFIED BLOCK ---
|
||||
// Use toDbJson() for a complete, consistent log
|
||||
final Map<String, dynamic> jsonData = data.toDbJson();
|
||||
jsonData['serverConfigName'] = serverName;
|
||||
// All other fields are now included in toDbJson()
|
||||
// --- END: MODIFIED BLOCK ---
|
||||
|
||||
final jsonFile = File(p.join(eventDir.path, 'data.json'));
|
||||
await jsonFile.writeAsString(jsonEncode(jsonData));
|
||||
debugPrint("Equipment Maintenance log saved to: ${jsonFile.path}");
|
||||
return eventDir.path;
|
||||
} catch (e) {
|
||||
debugPrint("Error saving Equipment Maintenance log to local storage: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getAllEquipmentMaintenanceLogs() async {
|
||||
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
|
||||
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
|
||||
|
||||
final List<Map<String, dynamic>> allLogs = [];
|
||||
final serverDirs = mmsv4Root.listSync().whereType<Directory>();
|
||||
|
||||
for (var serverDir in serverDirs) {
|
||||
final baseDir = Directory(p.join(serverDir.path, 'marine', 'marine_equipment_maintenance'));
|
||||
if (!await baseDir.exists()) continue;
|
||||
try {
|
||||
final entities = baseDir.listSync();
|
||||
for (var entity in entities) {
|
||||
if (entity is Directory) {
|
||||
final jsonFile = File(p.join(entity.path, 'data.json'));
|
||||
if (await jsonFile.exists()) {
|
||||
final data = jsonDecode(await jsonFile.readAsString()) as Map<String, dynamic>;
|
||||
data['logDirectory'] = entity.path;
|
||||
allLogs.add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error reading Equipment Maintenance logs from ${baseDir.path}: $e");
|
||||
}
|
||||
}
|
||||
return allLogs;
|
||||
}
|
||||
|
||||
Future<void> updateEquipmentMaintenanceLog(Map<String, dynamic> updatedLogData) async {
|
||||
final logDir = updatedLogData['logDirectory'];
|
||||
if (logDir == null) return;
|
||||
try {
|
||||
final jsonFile = File(p.join(logDir, 'data.json'));
|
||||
if (await jsonFile.exists()) {
|
||||
updatedLogData.remove('isResubmitting');
|
||||
await jsonFile.writeAsString(jsonEncode(updatedLogData));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error updating Equipment Maintenance log: $e");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =======================================================================
|
||||
// Part 5: River In-Situ Specific Methods (LOGGING RESTORED)
|
||||
// =======================================================================
|
||||
|
||||
@ -2,57 +2,259 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
|
||||
import '../auth_provider.dart';
|
||||
import '../models/marine_manual_equipment_maintenance_data.dart';
|
||||
import 'api_service.dart';
|
||||
import 'package:environment_monitoring_app/services/database_helper.dart';
|
||||
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
||||
import 'package:environment_monitoring_app/services/server_config_service.dart';
|
||||
import 'package:environment_monitoring_app/services/retry_service.dart';
|
||||
import 'package:environment_monitoring_app/services/submission_api_service.dart';
|
||||
|
||||
import 'base_api_service.dart'; // Import for SessionExpiredException
|
||||
|
||||
class MarineManualEquipmentMaintenanceService {
|
||||
final ApiService _apiService;
|
||||
// Use the new generic submission service
|
||||
final SubmissionApiService _submissionApiService = SubmissionApiService();
|
||||
final LocalStorageService _localStorageService = LocalStorageService();
|
||||
final ServerConfigService _serverConfigService = ServerConfigService();
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper();
|
||||
final RetryService _retryService = RetryService();
|
||||
|
||||
// Keep ApiService for getPreviousMaintenanceLogs
|
||||
final ApiService _apiService;
|
||||
MarineManualEquipmentMaintenanceService(this._apiService);
|
||||
|
||||
// *** START: Renamed this method ***
|
||||
/// Main submission method with online/offline branching logic
|
||||
Future<Map<String, dynamic>> submitMaintenanceReport({
|
||||
// *** END: Renamed this method ***
|
||||
required MarineManualEquipmentMaintenanceData data,
|
||||
required AuthProvider authProvider,
|
||||
List<Map<String, dynamic>>? appSettings, // Added for consistency
|
||||
BuildContext? context, // Added for consistency
|
||||
String? logDirectory,
|
||||
}) async {
|
||||
const String moduleName = 'marine_equipment_maintenance';
|
||||
|
||||
// --- START: ADDED LINE ---
|
||||
// Populate the user name from the AuthProvider
|
||||
data.conductedByUserName = authProvider.profileData?['first_name'] as String?;
|
||||
// --- END: ADDED LINE ---
|
||||
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
bool isOnline = connectivityResult != ConnectivityResult.none;
|
||||
bool isOfflineSession = authProvider.isLoggedIn &&
|
||||
(authProvider.profileData?['token']
|
||||
?.startsWith("offline-session-") ??
|
||||
false);
|
||||
|
||||
if (isOnline && isOfflineSession) {
|
||||
debugPrint(
|
||||
"$moduleName submission online during offline session. Attempting auto-relogin...");
|
||||
final bool transitionSuccess =
|
||||
await authProvider.checkAndTransitionToOnlineSession();
|
||||
if (transitionSuccess) {
|
||||
isOfflineSession = false;
|
||||
} else {
|
||||
isOnline = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isOnline && !isOfflineSession) {
|
||||
debugPrint("Proceeding with direct ONLINE $moduleName submission...");
|
||||
return await _performOnlineSubmission(
|
||||
data: data,
|
||||
moduleName: moduleName,
|
||||
authProvider: authProvider,
|
||||
logDirectory: logDirectory,
|
||||
);
|
||||
} else {
|
||||
debugPrint("Proceeding with OFFLINE $moduleName queuing mechanism...");
|
||||
return await _performOfflineQueuing(
|
||||
data: data,
|
||||
moduleName: moduleName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the direct online submission.
|
||||
Future<Map<String, dynamic>> _performOnlineSubmission({
|
||||
required MarineManualEquipmentMaintenanceData data,
|
||||
required String moduleName,
|
||||
required AuthProvider authProvider,
|
||||
String? logDirectory,
|
||||
}) async {
|
||||
final serverName =
|
||||
(await _serverConfigService.getActiveApiConfig())?['config_name']
|
||||
as String? ??
|
||||
'Default';
|
||||
Map<String, dynamic> apiResult;
|
||||
|
||||
try {
|
||||
// Call the existing method in MarineApiService
|
||||
return await _apiService.marine.submitMaintenanceLog(data);
|
||||
apiResult = await _submissionApiService.submitPost(
|
||||
moduleName: moduleName,
|
||||
endpoint: 'marine/maintenance', // Endpoint from marine_api_service.dart
|
||||
body: data.toApiFormData(),
|
||||
);
|
||||
} on SessionExpiredException {
|
||||
// Handle session expiry by attempting a silent relogin
|
||||
final bool reloginSuccess = await authProvider.attemptSilentRelogin();
|
||||
if (reloginSuccess) {
|
||||
// Retry the submission once if relogin was successful
|
||||
return await _apiService.marine.submitMaintenanceLog(data);
|
||||
apiResult = await _submissionApiService.submitPost(
|
||||
moduleName: moduleName,
|
||||
endpoint: 'marine/maintenance',
|
||||
body: data.toApiFormData(),
|
||||
);
|
||||
} else {
|
||||
return {
|
||||
apiResult = {
|
||||
'success': false,
|
||||
'message': 'Session expired. Please log in again.'
|
||||
};
|
||||
}
|
||||
} on SocketException {
|
||||
// Handle network errors
|
||||
return {
|
||||
} on SocketException catch (e) {
|
||||
apiResult = {
|
||||
'success': false,
|
||||
'message': 'Submission failed. Please check your network connection.'
|
||||
'message': "API submission failed with network error: $e"
|
||||
};
|
||||
} on TimeoutException {
|
||||
// Handle timeout errors
|
||||
return {
|
||||
// submission_api_service will queue this failure
|
||||
} on TimeoutException catch (e) {
|
||||
apiResult = {
|
||||
'success': false,
|
||||
'message': 'Submission timed out. Please check your network connection.'
|
||||
'message': "API submission timed out: $e"
|
||||
};
|
||||
// submission_api_service will queue this failure
|
||||
} catch (e) {
|
||||
// Handle any other unexpected errors
|
||||
return {'success': false, 'message': 'An unexpected error occurred: $e'};
|
||||
apiResult = {
|
||||
'success': false,
|
||||
'message': 'An unexpected error occurred: $e'
|
||||
};
|
||||
}
|
||||
|
||||
// Log the final result
|
||||
final bool overallSuccess = apiResult['success'] == true;
|
||||
final String finalMessage =
|
||||
apiResult['message'] ?? (overallSuccess ? 'Submission successful.' : 'Submission failed.');
|
||||
final String finalStatus = overallSuccess ? 'S4' : 'L1'; // S4 = API Success
|
||||
|
||||
if (overallSuccess) {
|
||||
// Assuming the API returns an ID. Adjust 'maintenance_id' if needed.
|
||||
data.reportId = apiResult['data']?['maintenance_id']?.toString();
|
||||
}
|
||||
|
||||
await _logAndSave(
|
||||
data: data,
|
||||
status: finalStatus,
|
||||
message: finalMessage,
|
||||
apiResult: apiResult,
|
||||
serverName: serverName,
|
||||
logDirectory: logDirectory,
|
||||
);
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
/// Handles saving the submission to local storage and queuing for retry.
|
||||
Future<Map<String, dynamic>> _performOfflineQueuing({
|
||||
required MarineManualEquipmentMaintenanceData data,
|
||||
required String moduleName,
|
||||
}) async {
|
||||
final serverConfig = await _serverConfigService.getActiveApiConfig();
|
||||
final serverName =
|
||||
serverConfig?['config_name'] as String? ?? 'Default';
|
||||
|
||||
data.submissionStatus = 'L1';
|
||||
data.submissionMessage = 'Equipment Maintenance queued due to being offline.';
|
||||
|
||||
// This method is added to LocalStorageService
|
||||
final String? localLogPath =
|
||||
await _localStorageService.saveEquipmentMaintenanceData(data, serverName: serverName);
|
||||
|
||||
if (localLogPath == null) {
|
||||
const message =
|
||||
"Failed to save Equipment Maintenance to local device storage.";
|
||||
await _logAndSave(
|
||||
data: data,
|
||||
status: 'Error',
|
||||
message: message,
|
||||
apiResult: {},
|
||||
serverName: serverName);
|
||||
return {'success': false, 'message': message};
|
||||
}
|
||||
|
||||
await _retryService.queueTask(
|
||||
type: 'equipment_maintenance_submission', // New task type
|
||||
payload: {
|
||||
'module': moduleName,
|
||||
'localLogPath': localLogPath,
|
||||
'serverConfig': serverConfig,
|
||||
},
|
||||
);
|
||||
|
||||
const successMessage =
|
||||
"No internet connection. Equipment Maintenance has been saved and queued for upload.";
|
||||
return {'success': true, 'message': successMessage};
|
||||
}
|
||||
|
||||
/// Logs the submission to the local file system and the central SQL database.
|
||||
Future<void> _logAndSave({
|
||||
required MarineManualEquipmentMaintenanceData data,
|
||||
required String status,
|
||||
required String message,
|
||||
required Map<String, dynamic> apiResult,
|
||||
required String serverName,
|
||||
String? logDirectory,
|
||||
}) async {
|
||||
data.submissionStatus = status;
|
||||
data.submissionMessage = message;
|
||||
|
||||
final fileTimestamp = data.maintenanceDate ?? DateTime.now().toIso8601String();
|
||||
|
||||
if (logDirectory != null) {
|
||||
// This is an update to an existing log file
|
||||
// --- START: MODIFIED BLOCK ---
|
||||
final Map<String, dynamic> updatedLogData = data.toDbJson();
|
||||
// Add metadata
|
||||
updatedLogData['submissionStatus'] = status;
|
||||
updatedLogData['submissionMessage'] = message;
|
||||
updatedLogData['logDirectory'] = logDirectory;
|
||||
updatedLogData['serverConfigName'] = serverName;
|
||||
updatedLogData['api_status'] = jsonEncode(apiResult);
|
||||
// All other fields (ysiSondeChecks, etc.) are now in toDbJson()
|
||||
// --- END: MODIFIED BLOCK ---
|
||||
|
||||
// This method is added to LocalStorageService
|
||||
await _localStorageService.updateEquipmentMaintenanceLog(updatedLogData);
|
||||
} else {
|
||||
// This is a new log
|
||||
// This method is added to LocalStorageService
|
||||
await _localStorageService.saveEquipmentMaintenanceData(data, serverName: serverName);
|
||||
}
|
||||
|
||||
final logData = {
|
||||
'submission_id': data.reportId ?? fileTimestamp,
|
||||
'module': 'marine',
|
||||
'type': 'Equipment Maintenance',
|
||||
'status': data.submissionStatus,
|
||||
'message': data.submissionMessage,
|
||||
'report_id': data.reportId,
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
// --- START: MODIFIED LINE ---
|
||||
'form_data': jsonEncode(data.toDbJson()), // Log the full DbJson
|
||||
// --- END: MODIFIED LINE ---
|
||||
'image_data': null, // No images
|
||||
'server_name': serverName,
|
||||
'api_status': jsonEncode(apiResult),
|
||||
'ftp_status': null, // No FTP
|
||||
};
|
||||
await _dbHelper.saveSubmissionLog(logData);
|
||||
}
|
||||
|
||||
/// Fetches previous maintenance logs to populate the form
|
||||
/// THIS METHOD IS UNCHANGED as it's a simple GET request.
|
||||
Future<Map<String, dynamic>> getPreviousMaintenanceLogs({
|
||||
required AuthProvider authProvider,
|
||||
}) async {
|
||||
|
||||
@ -2,53 +2,254 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
|
||||
import '../auth_provider.dart';
|
||||
import '../models/marine_manual_pre_departure_checklist_data.dart';
|
||||
import 'api_service.dart';
|
||||
import 'package:environment_monitoring_app/services/database_helper.dart';
|
||||
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
||||
import 'package:environment_monitoring_app/services/server_config_service.dart';
|
||||
import 'package:environment_monitoring_app/services/retry_service.dart';
|
||||
import 'package:environment_monitoring_app/services/submission_api_service.dart';
|
||||
|
||||
import 'base_api_service.dart'; // Import for SessionExpiredException
|
||||
|
||||
class MarineManualPreDepartureService {
|
||||
final ApiService _apiService;
|
||||
// Use the new generic submission service
|
||||
final SubmissionApiService _submissionApiService = SubmissionApiService();
|
||||
final LocalStorageService _localStorageService = LocalStorageService();
|
||||
final ServerConfigService _serverConfigService = ServerConfigService();
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper();
|
||||
final RetryService _retryService = RetryService();
|
||||
|
||||
MarineManualPreDepartureService(this._apiService);
|
||||
// The ApiService is kept only if other non-submission methods need it.
|
||||
// For this refactor, we'll remove it from the constructor.
|
||||
// final ApiService _apiService;
|
||||
// MarineManualPreDepartureService(this._apiService);
|
||||
MarineManualPreDepartureService(ApiService apiService); // Keep constructor signature for main.dart
|
||||
|
||||
/// Main submission method with online/offline branching logic
|
||||
Future<Map<String, dynamic>> submitChecklist({
|
||||
required MarineManualPreDepartureChecklistData data,
|
||||
required AuthProvider authProvider,
|
||||
List<Map<String, dynamic>>? appSettings, // Added for consistency
|
||||
BuildContext? context, // Added for consistency
|
||||
String? logDirectory,
|
||||
}) async {
|
||||
const String moduleName = 'marine_pre_departure';
|
||||
|
||||
// --- START: ADDED LINE ---
|
||||
// Populate the user name from the AuthProvider
|
||||
data.reporterName = authProvider.profileData?['first_name'] as String?;
|
||||
// --- END: ADDED LINE ---
|
||||
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
bool isOnline = connectivityResult != ConnectivityResult.none;
|
||||
bool isOfflineSession = authProvider.isLoggedIn &&
|
||||
(authProvider.profileData?['token']
|
||||
?.startsWith("offline-session-") ??
|
||||
false);
|
||||
|
||||
if (isOnline && isOfflineSession) {
|
||||
debugPrint(
|
||||
"$moduleName submission online during offline session. Attempting auto-relogin...");
|
||||
final bool transitionSuccess =
|
||||
await authProvider.checkAndTransitionToOnlineSession();
|
||||
if (transitionSuccess) {
|
||||
isOfflineSession = false;
|
||||
} else {
|
||||
isOnline = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isOnline && !isOfflineSession) {
|
||||
debugPrint("Proceeding with direct ONLINE $moduleName submission...");
|
||||
return await _performOnlineSubmission(
|
||||
data: data,
|
||||
moduleName: moduleName,
|
||||
authProvider: authProvider,
|
||||
logDirectory: logDirectory,
|
||||
);
|
||||
} else {
|
||||
debugPrint("Proceeding with OFFLINE $moduleName queuing mechanism...");
|
||||
return await _performOfflineQueuing(
|
||||
data: data,
|
||||
moduleName: moduleName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the direct online submission.
|
||||
Future<Map<String, dynamic>> _performOnlineSubmission({
|
||||
required MarineManualPreDepartureChecklistData data,
|
||||
required String moduleName,
|
||||
required AuthProvider authProvider,
|
||||
String? logDirectory,
|
||||
}) async {
|
||||
final serverName =
|
||||
(await _serverConfigService.getActiveApiConfig())?['config_name']
|
||||
as String? ??
|
||||
'Default';
|
||||
Map<String, dynamic> apiResult;
|
||||
|
||||
try {
|
||||
// Call the existing method in MarineApiService
|
||||
return await _apiService.marine.submitPreDepartureChecklist(data);
|
||||
apiResult = await _submissionApiService.submitPost(
|
||||
moduleName: moduleName,
|
||||
endpoint: 'marine/checklist', // Endpoint from marine_api_service.dart
|
||||
body: data.toApiFormData(),
|
||||
);
|
||||
} on SessionExpiredException {
|
||||
// Handle session expiry by attempting a silent relogin
|
||||
final bool reloginSuccess = await authProvider.attemptSilentRelogin();
|
||||
if (reloginSuccess) {
|
||||
// Retry the submission once if relogin was successful
|
||||
return await _apiService.marine.submitPreDepartureChecklist(data);
|
||||
apiResult = await _submissionApiService.submitPost(
|
||||
moduleName: moduleName,
|
||||
endpoint: 'marine/checklist',
|
||||
body: data.toApiFormData(),
|
||||
);
|
||||
} else {
|
||||
return {
|
||||
apiResult = {
|
||||
'success': false,
|
||||
'message': 'Session expired. Please log in again.'
|
||||
};
|
||||
}
|
||||
} on SocketException {
|
||||
// Handle network errors
|
||||
return {
|
||||
} on SocketException catch (e) {
|
||||
apiResult = {
|
||||
'success': false,
|
||||
'message': 'Submission failed. Please check your network connection.'
|
||||
'message': "API submission failed with network error: $e"
|
||||
};
|
||||
} on TimeoutException {
|
||||
// Handle timeout errors
|
||||
return {
|
||||
// submission_api_service will queue this failure
|
||||
} on TimeoutException catch (e) {
|
||||
apiResult = {
|
||||
'success': false,
|
||||
'message': 'Submission timed out. Please check your network connection.'
|
||||
'message': "API submission timed out: $e"
|
||||
};
|
||||
// submission_api_service will queue this failure
|
||||
} catch (e) {
|
||||
// Handle any other unexpected errors
|
||||
return {'success': false, 'message': 'An unexpected error occurred: $e'};
|
||||
}
|
||||
apiResult = {
|
||||
'success': false,
|
||||
'message': 'An unexpected error occurred: $e'
|
||||
};
|
||||
}
|
||||
|
||||
// Log the final result
|
||||
final bool overallSuccess = apiResult['success'] == true;
|
||||
final String finalMessage =
|
||||
apiResult['message'] ?? (overallSuccess ? 'Submission successful.' : 'Submission failed.');
|
||||
final String finalStatus = overallSuccess ? 'S4' : 'L1'; // S4 = API Success
|
||||
|
||||
if (overallSuccess) {
|
||||
// Assuming the API returns an ID. Adjust 'checklist_id' if needed.
|
||||
data.reportId = apiResult['data']?['checklist_id']?.toString();
|
||||
}
|
||||
|
||||
await _logAndSave(
|
||||
data: data,
|
||||
status: finalStatus,
|
||||
message: finalMessage,
|
||||
apiResult: apiResult,
|
||||
serverName: serverName,
|
||||
logDirectory: logDirectory,
|
||||
);
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
/// Handles saving the submission to local storage and queuing for retry.
|
||||
Future<Map<String, dynamic>> _performOfflineQueuing({
|
||||
required MarineManualPreDepartureChecklistData data,
|
||||
required String moduleName,
|
||||
}) async {
|
||||
final serverConfig = await _serverConfigService.getActiveApiConfig();
|
||||
final serverName =
|
||||
serverConfig?['config_name'] as String? ?? 'Default';
|
||||
|
||||
data.submissionStatus = 'L1';
|
||||
data.submissionMessage = 'Pre-Departure Checklist queued due to being offline.';
|
||||
|
||||
// This method is added to LocalStorageService
|
||||
final String? localLogPath =
|
||||
await _localStorageService.savePreDepartureData(data, serverName: serverName);
|
||||
|
||||
if (localLogPath == null) {
|
||||
const message =
|
||||
"Failed to save Pre-Departure Checklist to local device storage.";
|
||||
await _logAndSave(
|
||||
data: data,
|
||||
status: 'Error',
|
||||
message: message,
|
||||
apiResult: {},
|
||||
serverName: serverName);
|
||||
return {'success': false, 'message': message};
|
||||
}
|
||||
|
||||
await _retryService.queueTask(
|
||||
type: 'pre_departure_submission', // New task type
|
||||
payload: {
|
||||
'module': moduleName,
|
||||
'localLogPath': localLogPath,
|
||||
'serverConfig': serverConfig,
|
||||
},
|
||||
);
|
||||
|
||||
const successMessage =
|
||||
"No internet connection. Pre-Departure Checklist has been saved and queued for upload.";
|
||||
return {'success': true, 'message': successMessage};
|
||||
}
|
||||
|
||||
/// Logs the submission to the local file system and the central SQL database.
|
||||
Future<void> _logAndSave({
|
||||
required MarineManualPreDepartureChecklistData data,
|
||||
required String status,
|
||||
required String message,
|
||||
required Map<String, dynamic> apiResult,
|
||||
required String serverName,
|
||||
String? logDirectory,
|
||||
}) async {
|
||||
data.submissionStatus = status;
|
||||
data.submissionMessage = message;
|
||||
|
||||
final fileTimestamp = data.submissionDate ?? DateTime.now().toIso8601String();
|
||||
|
||||
if (logDirectory != null) {
|
||||
// This is an update to an existing log file
|
||||
// --- START: MODIFIED BLOCK ---
|
||||
final Map<String, dynamic> updatedLogData = data.toDbJson();
|
||||
// Add metadata
|
||||
updatedLogData['submissionStatus'] = status;
|
||||
updatedLogData['submissionMessage'] = message;
|
||||
updatedLogData['logDirectory'] = logDirectory;
|
||||
updatedLogData['serverConfigName'] = serverName;
|
||||
updatedLogData['api_status'] = jsonEncode(apiResult);
|
||||
// All other fields are now in toDbJson()
|
||||
// --- END: MODIFIED BLOCK ---
|
||||
|
||||
// This method is added to LocalStorageService
|
||||
await _localStorageService.updatePreDepartureLog(updatedLogData);
|
||||
} else {
|
||||
// This is a new log
|
||||
// This method is added to LocalStorageService
|
||||
await _localStorageService.savePreDepartureData(data, serverName: serverName);
|
||||
}
|
||||
|
||||
final logData = {
|
||||
'submission_id': data.reportId ?? fileTimestamp,
|
||||
'module': 'marine',
|
||||
'type': 'Pre-Departure Checklist',
|
||||
'status': data.submissionStatus,
|
||||
'message': data.submissionMessage,
|
||||
'report_id': data.reportId,
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
// --- START: MODIFIED LINE ---
|
||||
'form_data': jsonEncode(data.toDbJson()), // Log the full DbJson
|
||||
// --- END: MODIFIED LINE ---
|
||||
'image_data': null, // No images
|
||||
'server_name': serverName,
|
||||
'api_status': jsonEncode(apiResult),
|
||||
'ftp_status': null, // No FTP
|
||||
};
|
||||
await _dbHelper.saveSubmissionLog(logData);
|
||||
}
|
||||
}
|
||||
@ -2,53 +2,250 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
|
||||
import '../auth_provider.dart';
|
||||
import '../models/marine_manual_sonde_calibration_data.dart';
|
||||
import 'api_service.dart';
|
||||
import 'package:environment_monitoring_app/services/database_helper.dart';
|
||||
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
||||
import 'package:environment_monitoring_app/services/server_config_service.dart';
|
||||
import 'package:environment_monitoring_app/services/retry_service.dart';
|
||||
import 'package:environment_monitoring_app/services/submission_api_service.dart';
|
||||
|
||||
import 'base_api_service.dart'; // Import for SessionExpiredException
|
||||
|
||||
class MarineManualSondeCalibrationService {
|
||||
final ApiService _apiService;
|
||||
// Use the new generic submission service
|
||||
final SubmissionApiService _submissionApiService = SubmissionApiService();
|
||||
final LocalStorageService _localStorageService = LocalStorageService();
|
||||
final ServerConfigService _serverConfigService = ServerConfigService();
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper();
|
||||
final RetryService _retryService = RetryService();
|
||||
|
||||
MarineManualSondeCalibrationService(this._apiService);
|
||||
// The ApiService is kept only if other non-submission methods need it.
|
||||
// For this refactor, we'll remove it from the constructor.
|
||||
// final ApiService _apiService;
|
||||
// MarineManualSondeCalibrationService(this._apiService);
|
||||
MarineManualSondeCalibrationService(ApiService apiService); // Keep constructor signature for main.dart
|
||||
|
||||
/// Main submission method with online/offline branching logic
|
||||
Future<Map<String, dynamic>> submitCalibration({
|
||||
required MarineManualSondeCalibrationData data,
|
||||
required AuthProvider authProvider,
|
||||
List<Map<String, dynamic>>? appSettings, // Added for consistency
|
||||
BuildContext? context, // Added for consistency
|
||||
String? logDirectory,
|
||||
}) async {
|
||||
const String moduleName = 'marine_sonde_calibration';
|
||||
|
||||
// --- START: ADDED LINE ---
|
||||
// Populate the user name from the AuthProvider
|
||||
data.calibratedByUserName = authProvider.profileData?['first_name'] as String?;
|
||||
// --- END: ADDED LINE ---
|
||||
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
bool isOnline = connectivityResult != ConnectivityResult.none;
|
||||
bool isOfflineSession = authProvider.isLoggedIn &&
|
||||
(authProvider.profileData?['token']
|
||||
?.startsWith("offline-session-") ??
|
||||
false);
|
||||
|
||||
if (isOnline && isOfflineSession) {
|
||||
debugPrint(
|
||||
"$moduleName submission online during offline session. Attempting auto-relogin...");
|
||||
final bool transitionSuccess =
|
||||
await authProvider.checkAndTransitionToOnlineSession();
|
||||
if (transitionSuccess) {
|
||||
isOfflineSession = false;
|
||||
} else {
|
||||
isOnline = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isOnline && !isOfflineSession) {
|
||||
debugPrint("Proceeding with direct ONLINE $moduleName submission...");
|
||||
return await _performOnlineSubmission(
|
||||
data: data,
|
||||
moduleName: moduleName,
|
||||
authProvider: authProvider,
|
||||
logDirectory: logDirectory,
|
||||
);
|
||||
} else {
|
||||
debugPrint("Proceeding with OFFLINE $moduleName queuing mechanism...");
|
||||
return await _performOfflineQueuing(
|
||||
data: data,
|
||||
moduleName: moduleName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the direct online submission.
|
||||
Future<Map<String, dynamic>> _performOnlineSubmission({
|
||||
required MarineManualSondeCalibrationData data,
|
||||
required String moduleName,
|
||||
required AuthProvider authProvider,
|
||||
String? logDirectory,
|
||||
}) async {
|
||||
final serverName =
|
||||
(await _serverConfigService.getActiveApiConfig())?['config_name']
|
||||
as String? ??
|
||||
'Default';
|
||||
Map<String, dynamic> apiResult;
|
||||
|
||||
try {
|
||||
// Call the existing method in MarineApiService
|
||||
return await _apiService.marine.submitSondeCalibration(data);
|
||||
apiResult = await _submissionApiService.submitPost(
|
||||
moduleName: moduleName,
|
||||
endpoint: 'marine/calibration', // Endpoint from marine_api_service.dart
|
||||
body: data.toApiFormData(),
|
||||
);
|
||||
} on SessionExpiredException {
|
||||
// Handle session expiry by attempting a silent relogin
|
||||
final bool reloginSuccess = await authProvider.attemptSilentRelogin();
|
||||
if (reloginSuccess) {
|
||||
// Retry the submission once if relogin was successful
|
||||
return await _apiService.marine.submitSondeCalibration(data);
|
||||
apiResult = await _submissionApiService.submitPost(
|
||||
moduleName: moduleName,
|
||||
endpoint: 'marine/calibration',
|
||||
body: data.toApiFormData(),
|
||||
);
|
||||
} else {
|
||||
return {
|
||||
apiResult = {
|
||||
'success': false,
|
||||
'message': 'Session expired. Please log in again.'
|
||||
};
|
||||
}
|
||||
} on SocketException {
|
||||
// Handle network errors
|
||||
return {
|
||||
} on SocketException catch (e) {
|
||||
apiResult = {
|
||||
'success': false,
|
||||
'message': 'Submission failed. Please check your network connection.'
|
||||
'message': "API submission failed with network error: $e"
|
||||
};
|
||||
} on TimeoutException {
|
||||
// Handle timeout errors
|
||||
return {
|
||||
// submission_api_service will queue this failure
|
||||
} on TimeoutException catch (e) {
|
||||
apiResult = {
|
||||
'success': false,
|
||||
'message': 'Submission timed out. Please check your network connection.'
|
||||
'message': "API submission timed out: $e"
|
||||
};
|
||||
// submission_api_service will queue this failure
|
||||
} catch (e) {
|
||||
// Handle any other unexpected errors
|
||||
return {'success': false, 'message': 'An unexpected error occurred: $e'};
|
||||
}
|
||||
apiResult = {
|
||||
'success': false,
|
||||
'message': 'An unexpected error occurred: $e'
|
||||
};
|
||||
}
|
||||
|
||||
// Log the final result
|
||||
final bool overallSuccess = apiResult['success'] == true;
|
||||
final String finalMessage =
|
||||
apiResult['message'] ?? (overallSuccess ? 'Submission successful.' : 'Submission failed.');
|
||||
final String finalStatus = overallSuccess ? 'S4' : 'L1'; // S4 = API Success
|
||||
|
||||
if (overallSuccess) {
|
||||
// Assuming the API returns an ID. Adjust 'calibration_id' if needed.
|
||||
data.reportId = apiResult['data']?['calibration_id']?.toString();
|
||||
}
|
||||
|
||||
await _logAndSave(
|
||||
data: data,
|
||||
status: finalStatus,
|
||||
message: finalMessage,
|
||||
apiResult: apiResult,
|
||||
serverName: serverName,
|
||||
logDirectory: logDirectory,
|
||||
);
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
/// Handles saving the submission to local storage and queuing for retry.
|
||||
Future<Map<String, dynamic>> _performOfflineQueuing({
|
||||
required MarineManualSondeCalibrationData data,
|
||||
required String moduleName,
|
||||
}) async {
|
||||
final serverConfig = await _serverConfigService.getActiveApiConfig();
|
||||
final serverName =
|
||||
serverConfig?['config_name'] as String? ?? 'Default';
|
||||
|
||||
data.submissionStatus = 'L1';
|
||||
data.submissionMessage = 'Sonde Calibration queued due to being offline.';
|
||||
|
||||
// This method is added to LocalStorageService
|
||||
final String? localLogPath =
|
||||
await _localStorageService.saveSondeCalibrationData(data, serverName: serverName);
|
||||
|
||||
if (localLogPath == null) {
|
||||
const message =
|
||||
"Failed to save Sonde Calibration to local device storage.";
|
||||
await _logAndSave(
|
||||
data: data,
|
||||
status: 'Error',
|
||||
message: message,
|
||||
apiResult: {},
|
||||
serverName: serverName);
|
||||
return {'success': false, 'message': message};
|
||||
}
|
||||
|
||||
await _retryService.queueTask(
|
||||
type: 'sonde_calibration_submission', // New task type
|
||||
payload: {
|
||||
'module': moduleName,
|
||||
'localLogPath': localLogPath,
|
||||
'serverConfig': serverConfig,
|
||||
},
|
||||
);
|
||||
|
||||
const successMessage =
|
||||
"No internet connection. Sonde Calibration has been saved and queued for upload.";
|
||||
return {'success': true, 'message': successMessage};
|
||||
}
|
||||
|
||||
/// Logs the submission to the local file system and the central SQL database.
|
||||
Future<void> _logAndSave({
|
||||
required MarineManualSondeCalibrationData data,
|
||||
required String status,
|
||||
required String message,
|
||||
required Map<String, dynamic> apiResult,
|
||||
required String serverName,
|
||||
String? logDirectory,
|
||||
}) async {
|
||||
data.submissionStatus = status;
|
||||
data.submissionMessage = message;
|
||||
|
||||
final fileTimestamp = data.startDateTime?.replaceAll(':', '-').replaceAll(' ', '_') ?? DateTime.now().toIso8601String();
|
||||
|
||||
// --- START: MODIFIED BLOCK ---
|
||||
// Use the new toDbJson() method to get ALL data for logging
|
||||
final Map<String, dynamic> logDataMap = data.toDbJson();
|
||||
// Add submission-specific metadata
|
||||
logDataMap['api_status'] = jsonEncode(apiResult);
|
||||
logDataMap['serverConfigName'] = serverName;
|
||||
// --- END: MODIFIED BLOCK ---
|
||||
|
||||
|
||||
if (logDirectory != null) {
|
||||
// This is an update to an existing log file
|
||||
logDataMap['logDirectory'] = logDirectory; // Ensure logDirectory is in the map
|
||||
await _localStorageService.updateSondeCalibrationLog(logDataMap);
|
||||
} else {
|
||||
// This is a new log
|
||||
// Pass the complete data object, which now includes the user name
|
||||
await _localStorageService.saveSondeCalibrationData(data, serverName: serverName);
|
||||
}
|
||||
|
||||
final logData = {
|
||||
'submission_id': data.reportId ?? fileTimestamp,
|
||||
'module': 'marine',
|
||||
'type': 'Sonde Calibration',
|
||||
'status': data.submissionStatus,
|
||||
'message': data.submissionMessage,
|
||||
'report_id': data.reportId,
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
'form_data': jsonEncode(data.toDbJson()), // <-- Use toDbJson here
|
||||
'image_data': null, // No images
|
||||
'server_name': serverName,
|
||||
'api_status': jsonEncode(apiResult),
|
||||
'ftp_status': null, // No FTP
|
||||
};
|
||||
await _dbHelper.saveSubmissionLog(logData);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user