From fa5f0361dea63b3cb67eeea7af2fed27a1b50601 Mon Sep 17 00:00:00 2001 From: ALim Aidrus Date: Sat, 15 Nov 2025 15:44:21 +0800 Subject: [PATCH] repair marine report log menu --- lib/main.dart | 8 + ...ine_manual_equipment_maintenance_data.dart | 41 + ...e_manual_pre_departure_checklist_data.dart | 31 + .../marine_manual_sonde_calibration_data.dart | 46 + .../manual/marine_manual_data_status_log.dart | 376 ++--- .../marine_manual_report_status_log.dart | 1291 +++++++++++++++++ lib/screens/marine/marine_home_page.dart | 3 + lib/services/local_storage_service.dart | 269 +++- ..._manual_equipment_maintenance_service.dart | 236 ++- .../marine_manual_pre_departure_service.dart | 237 ++- ...rine_manual_sonde_calibration_service.dart | 233 ++- 11 files changed, 2461 insertions(+), 310 deletions(-) create mode 100644 lib/screens/marine/manual/marine_manual_report_status_log.dart diff --git a/lib/main.dart b/lib/main.dart index 6823d21..b12c870 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 { '/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(), diff --git a/lib/models/marine_manual_equipment_maintenance_data.dart b/lib/models/marine_manual_equipment_maintenance_data.dart index f04c4e6..9d5d5b8 100644 --- a/lib/models/marine_manual_equipment_maintenance_data.dart +++ b/lib/models/marine_manual_equipment_maintenance_data.dart @@ -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> 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 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 toApiFormData() { diff --git a/lib/models/marine_manual_pre_departure_checklist_data.dart b/lib/models/marine_manual_pre_departure_checklist_data.dart index e493613..24bb76b 100644 --- a/lib/models/marine_manual_pre_departure_checklist_data.dart +++ b/lib/models/marine_manual_pre_departure_checklist_data.dart @@ -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 checklistItems = {}; @@ -11,8 +16,31 @@ class MarineManualPreDepartureChecklistData { // Key: Item description, Value: Remarks text Map 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 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 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 }; } diff --git a/lib/models/marine_manual_sonde_calibration_data.dart b/lib/models/marine_manual_sonde_calibration_data.dart index e68e44b..471f4b9 100644 --- a/lib/models/marine_manual_sonde_calibration_data.dart +++ b/lib/models/marine_manual_sonde_calibration_data.dart @@ -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 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 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 --- } \ No newline at end of file diff --git a/lib/screens/marine/manual/marine_manual_data_status_log.dart b/lib/screens/marine/manual/marine_manual_data_status_log.dart index 4be3c73..1568c0e 100644 --- a/lib/screens/marine/manual/marine_manual_data_status_log.dart +++ b/lib/screens/marine/manual/marine_manual_data_status_log.dart @@ -68,21 +68,13 @@ class _MarineManualDataStatusLogState extends State { late MarineInSituSamplingService _marineInSituService; late MarineTarballSamplingService _marineTarballService; - // --- START: MODIFIED STATE FOR DROPDOWN --- - String _selectedModule = 'Manual Sampling'; - final List _modules = ['Manual Sampling', 'Tarball Sampling', 'Pre-Sampling', 'Report']; - final TextEditingController _searchController = TextEditingController(); - List _manualLogs = []; List _tarballLogs = []; - List _preSamplingLogs = []; // No data source, will be empty - List _reportLogs = []; // Will hold NPE logs - List _filteredManualLogs = []; List _filteredTarballLogs = []; - List _filteredPreSamplingLogs = []; - List _filteredReportLogs = []; - // --- END: MODIFIED STATE --- + + final TextEditingController _manualSearchController = TextEditingController(); + final TextEditingController _tarballSearchController = TextEditingController(); bool _isLoading = true; final Map _isResubmitting = {}; @@ -92,7 +84,8 @@ class _MarineManualDataStatusLogState extends State { 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 { 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(context, listen: false); _marineTarballService = Provider.of(context, listen: false); + // --- END FIX --- } @override void dispose() { - _searchController.dispose(); // Dispose single search controller + _manualSearchController.dispose(); + _tarballSearchController.dispose(); super.dispose(); } Future _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 tempManual = []; final List tempTarball = []; - final List tempReport = []; - final List 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 { )); } - // 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 { )); } - // --- 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 { 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 { } } - // --- 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( - 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 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 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 { // 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 { 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 { _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 { ], ), ), - // --- 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 { ); } - // --- 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 { title, style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 16, + fontSize: 14, color: Theme.of(context).primaryColor, ), ), @@ -636,11 +513,24 @@ class _MarineManualDataStatusLogState extends State { 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 { 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 { // --- 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 { 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 { 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 { ); } - // ========================================================================= - // --- END OF MODIFIED FUNCTION --- - // ========================================================================= - /// Shows the image gallery dialog void _showImageDialog(BuildContext context, SubmissionLogEntry log) { - // --- START: MODIFIED to handle all log types --- final List imageEntries = []; if (log.type == 'Manual Sampling') { @@ -839,7 +716,6 @@ class _MarineManualDataStatusLogState extends State { }; _addImagesToList(log, imageRemarkMap, imageEntries); } - // --- END: MODIFIED --- if (imageEntries.isEmpty) { @@ -952,7 +828,6 @@ class _MarineManualDataStatusLogState extends State { } } } - // --- END: NEW HELPER --- Widget _buildGranularStatus(String type, String? jsonStatus) { if (jsonStatus == null || jsonStatus.isEmpty) { @@ -999,18 +874,7 @@ class _MarineManualDataStatusLogState extends State { ), ); } - - 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 --- +// ========================================================================= } \ No newline at end of file diff --git a/lib/screens/marine/manual/marine_manual_report_status_log.dart b/lib/screens/marine/manual/marine_manual_report_status_log.dart new file mode 100644 index 0000000..1e8b3cd --- /dev/null +++ b/lib/screens/marine/manual/marine_manual_report_status_log.dart @@ -0,0 +1,1291 @@ +// lib/screens/marine/manual/marine_manual_report_status_log.dart + +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'dart:convert'; // Added for jsonDecode + +// Core Providers and Services +import 'package:environment_monitoring_app/auth_provider.dart'; +import 'package:environment_monitoring_app/services/local_storage_service.dart'; + +// Data Models for Reconstruction +import 'package:environment_monitoring_app/models/marine_manual_npe_report_data.dart'; +import 'package:environment_monitoring_app/models/marine_manual_pre_departure_checklist_data.dart'; +import 'package:environment_monitoring_app/models/marine_manual_sonde_calibration_data.dart'; +import 'package:environment_monitoring_app/models/marine_manual_equipment_maintenance_data.dart'; + +// Services for Resubmission +import 'package:environment_monitoring_app/services/marine_npe_report_service.dart'; +import 'package:environment_monitoring_app/services/marine_manual_pre_departure_service.dart'; +import 'package:environment_monitoring_app/services/marine_manual_sonde_calibration_service.dart'; +import 'package:environment_monitoring_app/services/marine_manual_equipment_maintenance_service.dart'; + +// --- START: ADDED HELPER CLASS --- +/// A simple class to hold an image file and its associated remark. +class ImageLogEntry { + final File file; + final String? remark; + ImageLogEntry({required this.file, this.remark}); +} +// --- END: ADDED HELPER CLASS --- + +/// A generic data holder for any log entry, identical to the one in the data status log. +class SubmissionLogEntry { + final String type; + final String title; + final String stationCode; + final String senderName; // <-- ADDED + final DateTime submissionDateTime; + final String? reportId; + final String status; + final String message; + final Map rawData; + final String serverName; + final String? apiStatusRaw; + final String? ftpStatusRaw; + bool isResubmitting; + + SubmissionLogEntry({ + required this.type, + required this.title, + required this.stationCode, + required this.senderName, // <-- ADDED + required this.submissionDateTime, + this.reportId, + required this.status, + required this.message, + required this.rawData, + required this.serverName, + this.apiStatusRaw, + this.ftpStatusRaw, + this.isResubmitting = false, + }); +} + +class MarineManualReportStatusLog extends StatefulWidget { + const MarineManualReportStatusLog({super.key}); + + @override + State createState() => + _MarineManualReportStatusLogState(); +} + +class _MarineManualReportStatusLogState + extends State { + // Services for loading and resubmitting + late LocalStorageService _localStorageService; + late MarineNpeReportService _npeReportService; + late MarineManualPreDepartureService _preDepartureService; + late MarineManualSondeCalibrationService _sondeCalibrationService; + late MarineManualEquipmentMaintenanceService _equipmentMaintenanceService; + + // State variables for logs, categorized as requested + List _preSamplingLogs = []; + List _reportLogs = []; + List _filteredPreSamplingLogs = []; + List _filteredReportLogs = []; + + final TextEditingController _preSamplingSearchController = + TextEditingController(); + final TextEditingController _reportSearchController = TextEditingController(); + + bool _isLoading = true; + final Map _isResubmitting = {}; + + @override + void initState() { + super.initState(); + // Controllers and initial load + _preSamplingSearchController.addListener(_filterLogs); + _reportSearchController.addListener(_filterLogs); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Initialize all required services from Provider + _localStorageService = Provider.of(context, listen: false); // Added listen: false + _npeReportService = Provider.of(context, listen: false); // Added listen: false + _preDepartureService = + Provider.of(context, listen: false); // Added listen: false + _sondeCalibrationService = + Provider.of(context, listen: false); // Added listen: false + _equipmentMaintenanceService = + Provider.of(context, listen: false); // Added listen: false + + // Load logs after services are initialized + if (_isLoading) { + _loadAllLogs(); + } + } + + @override + void dispose() { + _preSamplingSearchController.dispose(); + _reportSearchController.dispose(); + super.dispose(); + } + + /// Loads all logs from local storage and categorizes them. + Future _loadAllLogs() async { + setState(() => _isLoading = true); + + // Fetch all individual log types + final preDepartureLogs = + await _localStorageService.getAllPreDepartureLogs(); + final sondeCalibrationLogs = + await _localStorageService.getAllSondeCalibrationLogs(); + final equipmentMaintenanceLogs = + await _localStorageService.getAllEquipmentMaintenanceLogs(); + // *** START: Fixed method name *** + final npeReportLogs = await _localStorageService.getAllNpeLogs(); + // *** END: Fixed method name *** + + final List tempPreSampling = []; + final List tempReport = []; + + // 1. Process Pre-Departure Logs -> Pre-Sampling + for (var log in preDepartureLogs) { + final dateStr = log['submissionDate'] ?? ''; + tempPreSampling.add(SubmissionLogEntry( + type: 'Pre-Departure Checklist', + title: 'Pre-Departure Checklist', + // --- START: REVERTED --- + stationCode: 'N/A', // Reverted + // --- END: REVERTED --- + // --- START: MODIFIED --- + senderName: (log['reporterName'] as String?) ?? 'Unknown User', + // --- END: MODIFIED --- + submissionDateTime: DateTime.tryParse(dateStr) ?? + 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'], + )); + } + + // 2. Process Sonde Calibration Logs -> Pre-Sampling + for (var log in sondeCalibrationLogs) { + // --- START: MODIFIED --- + final dateStr = log['startDateTime'] ?? ''; + // --- END: MODIFIED --- + tempPreSampling.add(SubmissionLogEntry( + type: 'Sonde Calibration', + // --- START: MODIFIED LINE --- + title: 'Sonde Calibration', // Use module name as title + // --- END: MODIFIED LINE --- + stationCode: log['location'] ?? 'N/A', + // --- START: MODIFIED --- + senderName: (log['calibratedByUserName'] as String?) ?? 'Unknown User', + // --- END: MODIFIED --- + submissionDateTime: DateTime.tryParse(dateStr) ?? + 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'], + )); + } + + // 3. Process Equipment Maintenance Logs -> Pre-Sampling + for (var log in equipmentMaintenanceLogs) { + // --- START: MODIFIED --- + final dateStr = log['maintenanceDate'] ?? ''; + // --- END: MODIFIED --- + tempPreSampling.add(SubmissionLogEntry( + type: 'Equipment Maintenance', + title: 'Equipment Maintenance', + stationCode: log['location'] ?? 'N/A', + // --- START: MODIFIED --- + senderName: (log['conductedByUserName'] as String?) ?? 'Unknown User', + // --- END: MODIFIED --- + submissionDateTime: DateTime.tryParse(dateStr) ?? + 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'], + )); + } + + // 4. Process NPE Report Logs -> Report + for (var log in npeReportLogs) { + final dateStr = log['eventDate'] ?? ''; + final timeStr = log['eventTime'] ?? ''; + final title = log['selectedStation']?['man_station_name'] ?? + log['selectedStation']?['tbl_station_name'] ?? + log['locationDescription'] ?? + 'NPE Report'; + final stationCode = log['selectedStation']?['man_station_code'] ?? + log['selectedStation']?['tbl_station_code'] ?? + 'N/A'; + + tempReport.add(SubmissionLogEntry( + type: 'NPE Report', + title: title, + stationCode: stationCode, + // --- START: MODIFIED FOR NULL SAFETY --- + senderName: (log['firstSamplerName'] as String?) ?? 'Unknown User', // <-- FIXED + // --- END: MODIFIED FOR NULL SAFETY --- + submissionDateTime: DateTime.tryParse('$dateStr $timeStr') ?? + 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'], // NPE has FTP + )); + } + + // Sort logs by date (descending) + tempPreSampling + .sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime)); + tempReport.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime)); + + if (mounted) { + setState(() { + _preSamplingLogs = tempPreSampling; + _reportLogs = tempReport; + _isLoading = false; + }); + _filterLogs(); + } + } + + /// Filters logs based on search query. + void _filterLogs() { + final preSamplingQuery = _preSamplingSearchController.text.toLowerCase(); + final reportQuery = _reportSearchController.text.toLowerCase(); + + setState(() { + _filteredPreSamplingLogs = _preSamplingLogs + .where((log) => _logMatchesQuery(log, preSamplingQuery)) + .toList(); + _filteredReportLogs = _reportLogs + .where((log) => _logMatchesQuery(log, reportQuery)) + .toList(); + }); + } + + bool _logMatchesQuery(SubmissionLogEntry log, String query) { + if (query.isEmpty) return true; + return log.title.toLowerCase().contains(query) || + log.stationCode.toLowerCase().contains(query) || + log.serverName.toLowerCase().contains(query) || + log.type.toLowerCase().contains(query) || + log.senderName.toLowerCase().contains(query) || // <-- ADDED + (log.reportId?.toLowerCase() ?? '').contains(query); + } + + /// Helper to reconstruct a File object from a stored path. + File? _createFileFromPath(String? path) { + if (path != null && path.isNotEmpty) { + return File(path); + } + return null; + } + + /// Reconstructs the data model from raw log data and triggers resubmission. + Future _resubmitData(SubmissionLogEntry log) async { + final logKey = log.reportId ?? log.submissionDateTime.toIso8601String(); + if (mounted) { + setState(() { + _isResubmitting[logKey] = true; + }); + } + + try { + final authProvider = Provider.of(context, listen: false); + final appSettings = authProvider.appSettings; + + Map result = {}; + final logData = log.rawData; + + switch (log.type) { + case 'Pre-Departure Checklist': + final data = MarineManualPreDepartureChecklistData(); + data.reporterUserId = logData['reporterUserId']; + data.submissionDate = logData['submissionDate']; + // data.location = logData['location']; // <-- REVERTED + // Reconstruct maps + if (logData['checklistItems'] != null) { + data.checklistItems = + Map.from(logData['checklistItems']); + } + if (logData['remarks'] != null) { + data.remarks = Map.from(logData['remarks']); + } + result = await _preDepartureService.submitChecklist( + data: data, + authProvider: authProvider, + appSettings: appSettings, + context: context, + logDirectory: log.rawData['logDirectory'] as String?, + ); + break; + + case 'Sonde Calibration': + final data = MarineManualSondeCalibrationData(); + data.calibratedByUserId = logData['calibratedByUserId']; + data.sondeSerialNumber = logData['sondeSerialNumber']; + data.firmwareVersion = logData['firmwareVersion']; + data.korVersion = logData['korVersion']; + data.location = logData['location']; + data.startDateTime = logData['startDateTime']; + data.endDateTime = logData['endDateTime']; + data.ph7Mv = (logData['ph_7_mv'] as num?)?.toDouble(); // Fixed key + data.ph7Before = (logData['ph_7_before'] as num?)?.toDouble(); // Fixed key + data.ph7After = (logData['ph_7_after'] as num?)?.toDouble(); // Fixed key + data.ph10Mv = (logData['ph_10_mv'] as num?)?.toDouble(); // Fixed key + data.ph10Before = (logData['ph_10_before'] as num?)?.toDouble(); // Fixed key + data.ph10After = (logData['ph_10_after'] as num?)?.toDouble(); // Fixed key + data.condBefore = (logData['cond_before'] as num?)?.toDouble(); // Fixed key + data.condAfter = (logData['cond_after'] as num?)?.toDouble(); // Fixed key + data.doBefore = (logData['do_before'] as num?)?.toDouble(); // Fixed key + data.doAfter = (logData['do_after'] as num?)?.toDouble(); // Fixed key + data.turbidity0Before = (logData['turbidity_0_before'] as num?)?.toDouble(); // Fixed key + data.turbidity0After = (logData['turbidity_0_after'] as num?)?.toDouble(); // Fixed key + data.turbidity124Before = + (logData['turbidity_124_before'] as num?)?.toDouble(); // Fixed key + data.turbidity124After = + (logData['turbidity_124_after'] as num?)?.toDouble(); // Fixed key + data.calibrationStatus = logData['calibration_status']; // Fixed key + data.remarks = logData['remarks']; + + result = await _sondeCalibrationService.submitCalibration( + data: data, + authProvider: authProvider, + appSettings: appSettings, + context: context, + logDirectory: log.rawData['logDirectory'] as String?, + ); + break; + + case 'Equipment Maintenance': + final data = MarineManualEquipmentMaintenanceData(); + data.conductedByUserId = logData['conductedByUserId']; + data.maintenanceDate = logData['maintenanceDate']; + data.lastMaintenanceDate = logData['lastMaintenanceDate']; + data.scheduleMaintenance = logData['scheduleMaintenance']; + data.isReplacement = logData['isReplacement'] ?? false; + data.timeStart = logData['timeStart']; + data.timeEnd = logData['timeEnd']; + data.location = logData['location']; + data.ysiSondeComments = logData['ysiSondeComments']; + data.ysiSensorComments = logData['ysiSensorComments']; + data.vanDornComments = logData['vanDornComments']; + data.vanDornCurrentSerial = logData['vanDornCurrentSerial']; + data.vanDornNewSerial = logData['vanDornNewSerial']; + + // Reconstruct complex maps + if (logData['ysiSondeChecks'] != null) { + data.ysiSondeChecks = + Map.from(logData['ysiSondeChecks']); + } + if (logData['ysiSensorChecks'] != null) { + data.ysiSensorChecks = + (logData['ysiSensorChecks'] as Map).map( + (key, value) => MapEntry(key, Map.from(value)), + ); + } + if (logData['ysiReplacements'] != null) { + data.ysiReplacements = + (logData['ysiReplacements'] as Map).map( + (key, value) => MapEntry(key, Map.from(value)), + ); + } + if (logData['vanDornChecks'] != null) { + data.vanDornChecks = + (logData['vanDornChecks'] as Map).map( + (key, value) => MapEntry(key, Map.from(value)), + ); + } + if (logData['vanDornReplacements'] != null) { + data.vanDornReplacements = + (logData['vanDornReplacements'] as Map).map( + (key, value) => MapEntry(key, Map.from(value)), + ); + } + + // *** START: Fixed method name *** + result = await _equipmentMaintenanceService.submitMaintenanceReport( + // *** END: Fixed method name *** + data: data, + authProvider: authProvider, + appSettings: appSettings, + context: context, + logDirectory: log.rawData['logDirectory'] as String?, + ); + break; + + case 'NPE Report': + final data = MarineManualNpeReportData(); + data.firstSamplerName = logData['firstSamplerName']; + data.firstSamplerUserId = logData['firstSamplerUserId']; + data.eventDate = logData['eventDate']; + data.eventTime = logData['eventTime']; + data.locationDescription = logData['locationDescription']; + data.stateName = logData['stateName']; + data.selectedStation = logData['selectedStation']; + data.latitude = logData['latitude']; + data.longitude = logData['longitude']; + data.oxygenSaturation = (logData['oxygenSaturation'] as num?)?.toDouble(); + data.electricalConductivity = + (logData['electricalConductivity'] as num?)?.toDouble(); + data.oxygenConcentration = + (logData['oxygenConcentration'] as num?)?.toDouble(); + data.turbidity = (logData['turbidity'] as num?)?.toDouble(); + data.ph = (logData['ph'] as num?)?.toDouble(); + data.temperature = (logData['temperature'] as num?)?.toDouble(); + data.possibleSource = logData['possibleSource']; + data.image1Remark = logData['image1Remark']; + data.image2Remark = logData['image2Remark']; + data.image3Remark = logData['image3Remark']; + data.image4Remark = logData['image4Remark']; + data.tarballClassificationId = logData['tarballClassificationId']; + data.selectedTarballClassification = + logData['selectedTarballClassification']; + + // Reconstruct maps + if (logData['fieldObservations'] != null) { + data.fieldObservations = + Map.from(logData['fieldObservations']); + } + + // Reconstruct files from stored paths + data.image1 = _createFileFromPath(logData['image1Path']); + data.image2 = _createFileFromPath(logData['image2Path']); + data.image3 = _createFileFromPath(logData['image3Path']); + data.image4 = _createFileFromPath(logData['image4Path']); + + // *** START: Removed extra parameters *** + result = await _npeReportService.submitNpeReport( + data: data, + authProvider: authProvider, + logDirectory: log.rawData['logDirectory'] as String?, + ); + // *** END: Removed extra parameters *** + break; + + default: + throw Exception('Unknown log type for resubmission: ${log.type}'); + } + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + result['message'] ?? 'Resubmission process completed.')), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Resubmission failed: $e')), + ); + } + } finally { + if (mounted) { + setState(() { + _isResubmitting.remove(logKey); + }); + _loadAllLogs(); // Refresh the log list + } + } + } + + @override + Widget build(BuildContext context) { + final hasAnyLogs = _preSamplingLogs.isNotEmpty || _reportLogs.isNotEmpty; + + return Scaffold( + appBar: AppBar(title: const Text('Marine Report Status Log')), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : RefreshIndicator( + onRefresh: _loadAllLogs, + child: !hasAnyLogs + ? const Center(child: Text('No submission logs found.')) + : ListView( + padding: const EdgeInsets.all(8.0), + children: [ + _buildCategorySection( + 'Pre-Sampling Log', + _filteredPreSamplingLogs, + _preSamplingSearchController, + ), + _buildCategorySection( + 'Report Log', + _filteredReportLogs, + _reportSearchController, + ), + ], + ), + ), + ); + } + + /// Builds a collapsible card for a category of logs. + Widget _buildCategorySection( + String category, + List 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 this 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]); + }, + ), + ], + ), + ), + ); + } + + /// Builds an ExpansionTile for a single log entry. + Widget _buildLogListItem(SubmissionLogEntry log) { + final logKey = log.reportId ?? log.submissionDateTime.toIso8601String(); + final isResubmitting = _isResubmitting[logKey] ?? false; + + // Determine status icon and color + final bool isFullSuccess = log.status == 'S4'; + final bool isPartialSuccess = log.status == 'S3' || log.status == 'L4'; + final bool canResubmit = !isFullSuccess; + + IconData statusIcon; + Color statusColor; + + if (isFullSuccess) { + statusIcon = Icons.check_circle_outline; + statusColor = Colors.green; + } else if (isPartialSuccess) { + statusIcon = Icons.warning_amber_rounded; + statusColor = Colors.orange; + } else { + statusIcon = Icons.error_outline; + statusColor = Colors.red; + } + + final titleWidget = RichText( + text: TextSpan( + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(fontWeight: FontWeight.w500), + children: [ + TextSpan(text: '${log.title} '), + if (log.stationCode != 'N/A') + TextSpan( + text: '(${log.stationCode})', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(fontWeight: FontWeight.normal), + ), + ], + ), + ); + + // --- START: MODIFIED SUBTITLE --- + final bool isDateValid = !log.submissionDateTime + .isAtSameMomentAs(DateTime.fromMillisecondsSinceEpoch(0)); + final subtitle = isDateValid + ? '${log.senderName} - ${DateFormat('yyyy-MM-dd HH:mm').format(log.submissionDateTime)}' + : '${log.senderName} - Invalid Date'; + // --- END: MODIFIED SUBTITLE --- + + return ExpansionTile( + key: PageStorageKey(logKey), + leading: Icon(statusIcon, color: statusColor), + title: titleWidget, + subtitle: Text(subtitle), + trailing: canResubmit + ? (isResubmitting + ? const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator(strokeWidth: 3)) + : IconButton( + icon: const Icon(Icons.sync, color: Colors.blue), + tooltip: 'Resubmit', + onPressed: () => _resubmitData(log))) + : null, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDetailRow('Submission Type:', log.type), + _buildDetailRow('High-Level Status:', log.status), + _buildDetailRow('Server:', log.serverName), + _buildDetailRow('Report ID:', log.reportId ?? 'N/A'), + _buildGranularStatus('API', log.apiStatusRaw), // <-- MODIFIED + _buildGranularStatus('FTP', log.ftpStatusRaw), // <-- MODIFIED + _buildDetailRow('Message:', log.message), + + // --- START: ADDED BUTTONS --- + const Divider(height: 10), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton.icon( + icon: Icon(Icons.list_alt, color: Theme.of(context).colorScheme.primary), + label: Text('View Data', style: TextStyle(color: Theme.of(context).colorScheme.primary)), + onPressed: () => _showDataDialog(context, log), + ), + TextButton.icon( + icon: Icon(Icons.photo_library_outlined, color: Theme.of(context).colorScheme.secondary), + label: Text('View Images', style: TextStyle(color: Theme.of(context).colorScheme.secondary)), + onPressed: () => _showImageDialog(context, log), + ), + ], + ), + ), + // --- END: ADDED BUTTONS --- + ], + ), + ) + ], + ); + } + + 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: MODIFIED SECTION FOR VIEW DATA / VIEW IMAGE FUNCTIONALITY --- + // ========================================================================= + + /// Helper to safely get a string value from the raw data map. + String? _getString(Map data, String key) { + final value = data[key]; + if (value == null) return null; + return value.toString(); + } + + /// Builds a formatted category header row for the data list. + Widget _buildCategoryHeader(BuildContext context, String title, IconData icon) { + return Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 8.0), + child: Row( + children: [ + Icon(icon, size: 20, color: Theme.of(context).primaryColor), + const SizedBox(width: 8), + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Theme.of(context).primaryColor, + ), + ), + ], + ), + ); + } + + /// Builds a formatted row for the data list. + Widget _buildDataRow(String label, String? value) { + String displayValue = (value == null || value.isEmpty || value == 'null') ? 'N/A' : value; + + if (displayValue == '-999.0' || displayValue == '-999') { + displayValue = 'N/A'; + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: Text( + label, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.0, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + flex: 3, + child: Text( + displayValue, + style: const TextStyle(fontSize: 14.0), + ), + ), + ], + ), + ); + } + + // --- START: NEW HELPER FOR PRE-DEPARTURE --- + /// Determines the category for a given checklist item. + String _getChecklistCategory(String itemName) { + final lowerItem = itemName.toLowerCase(); + if (lowerItem.contains('in-situ') || lowerItem.contains('sonde') || lowerItem.contains('van dorn')) { + return 'Internal - In-Situ Sampling'; + } + if (lowerItem.contains('tarball')) { + return 'Internal - Tarball Sampling'; + } + if (lowerItem.contains('laboratory') || lowerItem.contains('ice') || lowerItem.contains('bottle')) { + return 'External - Laboratory'; + } + // Add more rules here if needed + return 'General'; + } + // --- END: NEW HELPER FOR PRE-DEPARTURE --- + + /// Shows the categorized and formatted data log in a dialog + void _showDataDialog(BuildContext context, SubmissionLogEntry log) { + final Map data = log.rawData; + Widget dialogContent; // This will hold either a ListView or a Column + + if (log.type == 'Pre-Departure Checklist') { + // --- START: Handle Pre-Departure Checklist (uses ListView) --- + final items = Map.from(data['checklistItems'] ?? {}); + final remarks = Map.from(data['remarks'] ?? {}); + + // 1. Group items by category + final Map>> categorizedItems = {}; + for (final entry in items.entries) { + final category = _getChecklistCategory(entry.key); + if (!categorizedItems.containsKey(category)) { + categorizedItems[category] = []; + } + categorizedItems[category]!.add(entry); + } + + // 2. Define the order + const categoryOrder = [ + 'Internal - In-Situ Sampling', + 'Internal - Tarball Sampling', + 'External - Laboratory', + 'General' + ]; + + // 3. Build the list of widgets + final List contentWidgets = []; + for (final category in categoryOrder) { + if (categorizedItems.containsKey(category) && categorizedItems[category]!.isNotEmpty) { + // Add the category header + contentWidgets.add(_buildCategoryHeader(context, category, Icons.check_box_outlined)); + + // Add the items for that category + contentWidgets.addAll( + categorizedItems[category]!.map((entry) { + final key = entry.key; + final value = entry.value; + final status = value ? 'Yes' : 'No'; + final remark = remarks[key] ?? ''; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Row 1: Item and Status + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Item Name + Expanded( + flex: 3, + child: Text( + key, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.0, + ), + ), + ), + const SizedBox(width: 8), + // Status + Expanded( + flex: 1, + child: Text( + status, + style: TextStyle( + fontSize: 14.0, + color: value ? Colors.green.shade700 : Colors.red.shade700, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.end, + ), + ), + ], + ), + // Row 2: Remark (only if it exists) + if (remark.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 6.0, left: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Remark: ", + style: TextStyle( + fontSize: 13.0, + color: Colors.grey.shade700, + fontStyle: FontStyle.italic, + ), + ), + Expanded( + child: Text( + remark, + style: TextStyle( + fontSize: 13.0, + color: Colors.grey.shade700, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + ), + ], + ), + ); + }).toList() + ); + + // Add a divider after the category + contentWidgets.add(const Divider(height: 16)); + } + } + + if (contentWidgets.isEmpty) { + dialogContent = const Center(child: Text('No checklist items found.')); + } else { + // Build the final Column + dialogContent = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: contentWidgets, + ); + } + // --- END: Handle Pre-Departure Checklist --- + + } else { + // --- START: Handle ALL OTHER Log Types (uses Column) --- + final List contentWidgets = []; + + // --- Helper for nested maps --- + void addNestedMapRows(Map map) { + map.forEach((key, value) { + if (value is Map) { + // Handle nested maps (e.g., ysiSensorChecks) + contentWidgets.add(_buildDataRow(key, '')); + value.forEach((subKey, subValue) { + contentWidgets.add( + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: _buildDataRow(subKey, subValue?.toString() ?? 'N/A'), + ) + ); + }); + } else { + contentWidgets.add(_buildDataRow(key, value?.toString() ?? 'N/A')); + } + }); + } + // --- End Helper --- + + switch (log.type) { + case 'Sonde Calibration': + contentWidgets.add(_buildCategoryHeader(context, 'Sonde Info', Icons.info_outline)); + contentWidgets.add(_buildDataRow('Sonde Serial #', _getString(data, 'sondeSerialNumber'))); + contentWidgets.add(_buildDataRow('Firmware Version', _getString(data, 'firmwareVersion'))); + contentWidgets.add(_buildDataRow('KOR Version', _getString(data, 'korVersion'))); + contentWidgets.add(_buildDataRow('Location', _getString(data, 'location'))); + contentWidgets.add(_buildDataRow('Start Time', _getString(data, 'startDateTime'))); + contentWidgets.add(_buildDataRow('End Time', _getString(data, 'endDateTime'))); + contentWidgets.add(_buildDataRow('Status', _getString(data, 'calibration_status'))); + contentWidgets.add(_buildDataRow('Remarks', _getString(data, 'remarks'))); + + contentWidgets.add(_buildCategoryHeader(context, 'pH 7.0', Icons.science_outlined)); + contentWidgets.add(_buildDataRow('MV', _getString(data, 'ph_7_mv'))); + contentWidgets.add(_buildDataRow('Before', _getString(data, 'ph_7_before'))); + contentWidgets.add(_buildDataRow('After', _getString(data, 'ph_7_after'))); + + contentWidgets.add(_buildCategoryHeader(context, 'pH 10.0', Icons.science_outlined)); + contentWidgets.add(_buildDataRow('MV', _getString(data, 'ph_10_mv'))); + contentWidgets.add(_buildDataRow('Before', _getString(data, 'ph_10_before'))); + contentWidgets.add(_buildDataRow('After', _getString(data, 'ph_10_after'))); + + contentWidgets.add(_buildCategoryHeader(context, 'Conductivity', Icons.thermostat)); + contentWidgets.add(_buildDataRow('Before', _getString(data, 'cond_before'))); + contentWidgets.add(_buildDataRow('After', _getString(data, 'cond_after'))); + + contentWidgets.add(_buildCategoryHeader(context, 'Dissolved Oxygen', Icons.air)); + contentWidgets.add(_buildDataRow('Before', _getString(data, 'do_before'))); + contentWidgets.add(_buildDataRow('After', _getString(data, 'do_after'))); + + contentWidgets.add(_buildCategoryHeader(context, 'Turbidity', Icons.waves)); + contentWidgets.add(_buildDataRow('0 NTU Before', _getString(data, 'turbidity_0_before'))); + contentWidgets.add(_buildDataRow('0 NTU After', _getString(data, 'turbidity_0_after'))); + contentWidgets.add(_buildDataRow('124 NTU Before', _getString(data, 'turbidity_124_before'))); + contentWidgets.add(_buildDataRow('124 NTU After', _getString(data, 'turbidity_124_after'))); + break; + + case 'Equipment Maintenance': + contentWidgets.add(_buildCategoryHeader(context, 'YSI Sonde Checks', Icons.build_circle_outlined)); + if (data['ysiSondeChecks'] != null) { + addNestedMapRows(Map.from(data['ysiSondeChecks'])); + } + contentWidgets.add(_buildDataRow('Comments', _getString(data, 'ysiSondeComments'))); + + contentWidgets.add(_buildCategoryHeader(context, 'YSI Sensor Checks', Icons.sensors)); + if (data['ysiSensorChecks'] != null) { + addNestedMapRows(Map.from(data['ysiSensorChecks'])); + } + contentWidgets.add(_buildDataRow('Comments', _getString(data, 'ysiSensorComments'))); + + contentWidgets.add(_buildCategoryHeader(context, 'YSI Replacements', Icons.published_with_changes)); + if (data['ysiReplacements'] != null) { + addNestedMapRows(Map.from(data['ysiReplacements'])); + } + + contentWidgets.add(_buildCategoryHeader(context, 'Van Dorn Checks', Icons.opacity)); + if (data['vanDornChecks'] != null) { + addNestedMapRows(Map.from(data['vanDornChecks'])); + } + contentWidgets.add(_buildDataRow('Comments', _getString(data, 'vanDornComments'))); + contentWidgets.add(_buildDataRow('Current Serial', _getString(data, 'vanDornCurrentSerial'))); + contentWidgets.add(_buildDataRow('New Serial', _getString(data, 'vanDornNewSerial'))); + + contentWidgets.add(_buildCategoryHeader(context, 'Van Dorn Replacements', Icons.published_with_changes)); + if (data['vanDornReplacements'] != null) { + addNestedMapRows(Map.from(data['vanDornReplacements'])); + } + break; + + case 'NPE Report': + contentWidgets.add(_buildCategoryHeader(context, 'Event Info', Icons.calendar_today)); + contentWidgets.add(_buildDataRow('Date', _getString(data, 'eventDate'))); + contentWidgets.add(_buildDataRow('Time', _getString(data, 'eventTime'))); + contentWidgets.add(_buildDataRow('Sampler', _getString(data, 'firstSamplerName'))); + + contentWidgets.add(_buildCategoryHeader(context, 'Location', Icons.location_on_outlined)); + if (data['selectedStation'] != null) { + contentWidgets.add(_buildDataRow('Station', _getString(data['selectedStation'], 'man_station_name') ?? _getString(data['selectedStation'], 'tbl_station_name'))); + } else { + contentWidgets.add(_buildDataRow('Location', _getString(data, 'locationDescription'))); + contentWidgets.add(_buildDataRow('State', _getString(data, 'stateName'))); + } + contentWidgets.add(_buildDataRow('Latitude', _getString(data, 'latitude'))); + contentWidgets.add(_buildDataRow('Longitude', _getString(data, 'longitude'))); + + contentWidgets.add(_buildCategoryHeader(context, 'Parameters', Icons.bar_chart)); + contentWidgets.add(_buildDataRow('Oxygen Conc (mg/L)', _getString(data, 'oxygenConcentration'))); + contentWidgets.add(_buildDataRow('Oxygen Sat (%)', _getString(data, 'oxygenSaturation'))); + contentWidgets.add(_buildDataRow('pH', _getString(data, 'ph'))); + contentWidgets.add(_buildDataRow('Conductivity (µS/cm)', _getString(data, 'electricalConductivity'))); + contentWidgets.add(_buildDataRow('Temperature (°C)', _getString(data, 'temperature'))); + contentWidgets.add(_buildDataRow('Turbidity (NTU)', _getString(data, 'turbidity'))); + + contentWidgets.add(_buildCategoryHeader(context, 'Observations', Icons.warning_amber_rounded)); + if (data['fieldObservations'] != null) { + final observations = Map.from(data['fieldObservations']); + observations.forEach((key, value) { + if(value) contentWidgets.add(_buildDataRow(key, 'Checked')); + }); + } + contentWidgets.add(_buildDataRow('Other Remarks', _getString(data, 'othersObservationRemark'))); + contentWidgets.add(_buildDataRow('Possible Source', _getString(data, 'possibleSource'))); + if (data['selectedTarballClassification'] != null) { + contentWidgets.add(_buildDataRow('Tarball Class', _getString(data['selectedTarballClassification'], 'classification_name'))); + } + break; + + default: + contentWidgets.add(_buildDataRow('Error', 'No data view configured for log type: ${log.type}')); + } + + // Assign the Column as the content for the 'else' block + dialogContent = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: contentWidgets, + ); + // --- END: Handle ALL OTHER Log Types --- + } + + // Now, dialogContent is guaranteed to be assigned + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('${log.title} (${log.stationCode})'), + content: SizedBox( + width: double.maxFinite, + child: SingleChildScrollView( + child: dialogContent, // <-- This is now safe + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ); + }, + ); + } + // --- END: MODIFIED METHOD --- + + + /// Shows the image gallery dialog + void _showImageDialog(BuildContext context, SubmissionLogEntry log) { + final List imageEntries = []; + + if (log.type == 'NPE Report') { + // NPE images are stored with 'image#Path' keys in the log + const imageRemarkMap = { + 'image1Path': 'image1Remark', + 'image2Path': 'image2Remark', + 'image3Path': 'image3Remark', + 'image4Path': 'image4Remark', + }; + _addImagesToList(log, imageRemarkMap, imageEntries); + } + + if (imageEntries.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No images are attached to this log.'), + backgroundColor: Colors.orange, + ), + ); + return; + } + + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Images for ${log.title}'), + content: SizedBox( + width: double.maxFinite, + child: GridView.builder( + shrinkWrap: true, + itemCount: imageEntries.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemBuilder: (context, index) { + final imageEntry = imageEntries[index]; + final bool hasRemark = imageEntry.remark != null && imageEntry.remark!.isNotEmpty; + + return Card( + clipBehavior: Clip.antiAlias, + elevation: 2, + child: Stack( + fit: StackFit.expand, + children: [ + Image.file( + imageEntry.file, + fit: BoxFit.cover, + errorBuilder: (context, error, stack) { + return const Center( + child: Icon( + Icons.broken_image, + color: Colors.grey, + size: 40, + ), + ); + }, + ), + if (hasRemark) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: const EdgeInsets.all(6.0), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black.withOpacity(0.8), + Colors.black.withOpacity(0.0) + ], + ), + ), + child: Text( + imageEntry.remark!, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ); + }, + ); + } + + /// Helper for _showImageDialog + void _addImagesToList(SubmissionLogEntry log, Map imageRemarkMap, List imageEntries) { + for (final entry in imageRemarkMap.entries) { + final imageKey = entry.key; + final remarkKey = entry.value; + + final path = log.rawData[imageKey]; + if (path != null && path is String && path.isNotEmpty) { + final file = File(path); + if (file.existsSync()) { + final remark = (remarkKey != null ? log.rawData[remarkKey] as String? : null); + imageEntries.add(ImageLogEntry(file: file, remark: remark)); + } + } + } + } + + /// Helper to build granular status (copied from data log) + Widget _buildGranularStatus(String type, String? jsonStatus) { + if (jsonStatus == null || jsonStatus.isEmpty) { + // Return an empty row for "API Status: N/A" if it was the default + if (type == 'API') { + return _buildDetailRow('API Status:', 'N/A'); + } + return Container(); + } + + List statuses; + try { + statuses = jsonDecode(jsonStatus); + } catch (_) { + return _buildDetailRow('$type Status:', jsonStatus); + } + + if (statuses.isEmpty) { + if (type == 'API') { + return _buildDetailRow('API Status:', 'N/A'); + } + return Container(); + } + + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('$type Status:', style: const TextStyle(fontWeight: FontWeight.bold)), + ...statuses.map((s) { + final serverName = s['server_name'] ?? s['config_name'] ?? 'Server N/A'; + final status = s['message'] ?? 'N/A'; + final bool isSuccess = s['success'] as bool? ?? false; + final IconData icon = isSuccess ? Icons.check_circle_outline : Icons.error_outline; + final Color color = isSuccess ? Colors.green : Colors.red; + String detailLabel = (s['type'] != null) ? '(${s['type']})' : ''; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 8.0), + child: Row( + children: [ + Icon(icon, size: 16, color: color), + const SizedBox(width: 5), + Expanded(child: Text('$serverName $detailLabel: $status')), + ], + ), + ); + }).toList(), + ], + ), + ); + } + +// ========================================================================= +// --- END: MODIFIED SECTION --- +// ========================================================================= +} \ No newline at end of file diff --git a/lib/screens/marine/marine_home_page.dart b/lib/screens/marine/marine_home_page.dart index 08f2cc0..80f2920 100644 --- a/lib/screens/marine/marine_home_page.dart +++ b/lib/screens/marine/marine_home_page.dart @@ -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( diff --git a/lib/services/local_storage_service.dart b/lib/services/local_storage_service.dart index d7269fb..b3abc52 100644 --- a/lib/services/local_storage_service.dart +++ b/lib/services/local_storage_service.dart @@ -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 _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 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 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>> getAllPreDepartureLogs() async { + final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); + if (mmsv4Root == null || !await mmsv4Root.exists()) return []; + + final List> allLogs = []; + final serverDirs = mmsv4Root.listSync().whereType(); + + 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; + data['logDirectory'] = entity.path; + allLogs.add(data); + } + } + } + } catch (e) { + debugPrint("Error reading Pre-Departure logs from ${baseDir.path}: $e"); + } + } + return allLogs; + } + + Future updatePreDepartureLog(Map 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 _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 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 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>> getAllSondeCalibrationLogs() async { + final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); + if (mmsv4Root == null || !await mmsv4Root.exists()) return []; + + final List> allLogs = []; + final serverDirs = mmsv4Root.listSync().whereType(); + + 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; + data['logDirectory'] = entity.path; + allLogs.add(data); + } + } + } + } catch (e) { + debugPrint("Error reading Sonde Calibration logs from ${baseDir.path}: $e"); + } + } + return allLogs; + } + + Future updateSondeCalibrationLog(Map 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 _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 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 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>> getAllEquipmentMaintenanceLogs() async { + final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); + if (mmsv4Root == null || !await mmsv4Root.exists()) return []; + + final List> allLogs = []; + final serverDirs = mmsv4Root.listSync().whereType(); + + 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; + data['logDirectory'] = entity.path; + allLogs.add(data); + } + } + } + } catch (e) { + debugPrint("Error reading Equipment Maintenance logs from ${baseDir.path}: $e"); + } + } + return allLogs; + } + + Future updateEquipmentMaintenanceLog(Map 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) // ======================================================================= diff --git a/lib/services/marine_manual_equipment_maintenance_service.dart b/lib/services/marine_manual_equipment_maintenance_service.dart index fbf8be4..8896548 100644 --- a/lib/services/marine_manual_equipment_maintenance_service.dart +++ b/lib/services/marine_manual_equipment_maintenance_service.dart @@ -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> submitMaintenanceReport({ + // *** END: Renamed this method *** required MarineManualEquipmentMaintenanceData data, required AuthProvider authProvider, + List>? 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> _performOnlineSubmission({ + required MarineManualEquipmentMaintenanceData data, + required String moduleName, + required AuthProvider authProvider, + String? logDirectory, + }) async { + final serverName = + (await _serverConfigService.getActiveApiConfig())?['config_name'] + as String? ?? + 'Default'; + Map 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> _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 _logAndSave({ + required MarineManualEquipmentMaintenanceData data, + required String status, + required String message, + required Map 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 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> getPreviousMaintenanceLogs({ required AuthProvider authProvider, }) async { diff --git a/lib/services/marine_manual_pre_departure_service.dart b/lib/services/marine_manual_pre_departure_service.dart index ad0827d..ba09ba3 100644 --- a/lib/services/marine_manual_pre_departure_service.dart +++ b/lib/services/marine_manual_pre_departure_service.dart @@ -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> submitChecklist({ required MarineManualPreDepartureChecklistData data, required AuthProvider authProvider, + List>? 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> _performOnlineSubmission({ + required MarineManualPreDepartureChecklistData data, + required String moduleName, + required AuthProvider authProvider, + String? logDirectory, + }) async { + final serverName = + (await _serverConfigService.getActiveApiConfig())?['config_name'] + as String? ?? + 'Default'; + Map 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> _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 _logAndSave({ + required MarineManualPreDepartureChecklistData data, + required String status, + required String message, + required Map 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 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); } } \ No newline at end of file diff --git a/lib/services/marine_manual_sonde_calibration_service.dart b/lib/services/marine_manual_sonde_calibration_service.dart index 690bb51..2fbf44a 100644 --- a/lib/services/marine_manual_sonde_calibration_service.dart +++ b/lib/services/marine_manual_sonde_calibration_service.dart @@ -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> submitCalibration({ required MarineManualSondeCalibrationData data, required AuthProvider authProvider, + List>? 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> _performOnlineSubmission({ + required MarineManualSondeCalibrationData data, + required String moduleName, + required AuthProvider authProvider, + String? logDirectory, + }) async { + final serverName = + (await _serverConfigService.getActiveApiConfig())?['config_name'] + as String? ?? + 'Default'; + Map 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> _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 _logAndSave({ + required MarineManualSondeCalibrationData data, + required String status, + required String message, + required Map 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 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); } } \ No newline at end of file