repair marine report log menu

This commit is contained in:
ALim Aidrus 2025-11-15 15:44:21 +08:00
parent da892821a2
commit fa5f0361de
11 changed files with 2461 additions and 310 deletions

View File

@ -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(),

View File

@ -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() {

View File

@ -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
};
}

View File

@ -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 ---
}

View File

@ -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(
padding: const EdgeInsets.all(8.0),
child: Column(
: RefreshIndicator(
onRefresh: _loadAllLogs,
child: !hasAnyLogs
? const Center(child: Text('No submission logs found.'))
: ListView(
padding: const EdgeInsets.all(8.0),
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
}
},
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,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
children: [
// --- SEARCH BAR (MOVED INSIDE CARD) ---
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search in $_selectedModule...',
prefixIcon: const Icon(Icons.search, size: 20),
isDense: true,
border: const OutlineInputBorder(),
suffixIcon: _searchController.text.isNotEmpty ? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
},
) : null,
),
),
),
const Divider(height: 1),
// --- LOG LIST (MOVED INSIDE CARD) ---
Expanded(
child: RefreshIndicator(
onRefresh: _loadAllLogs,
child: _buildCurrentModuleList(),
),
),
],
),
),
),
_buildCategorySection('Manual Sampling', _filteredManualLogs, _manualSearchController),
_buildCategorySection('Tarball Sampling', _filteredTarballLogs, _tarballSearchController),
],
),
),
);
}
// --- 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,
itemBuilder: (context, index) {
final log = filteredLogs[index];
return _buildLogListItem(log);
},
Widget _buildCategorySection(String category, List<SubmissionLogEntry> logs, TextEditingController searchController) {
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: searchController,
decoration: InputDecoration(
hintText: 'Search in $category...',
prefixIcon: const Icon(Icons.search, size: 20),
isDense: true,
border: const OutlineInputBorder(),
suffixIcon: searchController.text.isNotEmpty ? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
searchController.clear();
},
) : null,
),
),
),
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) {
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 ---
// =========================================================================
}

File diff suppressed because it is too large Load Diff

View File

@ -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(

View File

@ -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)
// =======================================================================

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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);
}
}