From d0f9d72ebd9ba0b3a73917261c57c3d6c3b29669 Mon Sep 17 00:00:00 2001 From: ALim Aidrus Date: Fri, 21 Nov 2025 09:00:37 +0800 Subject: [PATCH] modfiy marine npe report to properly send api and telegram alert to server --- lib/main.dart | 252 ++---- lib/models/marine_manual_npe_report_data.dart | 32 +- .../river_manual_triennial_sampling_data.dart | 57 ++ .../marine_manual_report_status_log.dart | 721 ++++++++++-------- .../reports/npe_report_from_in_situ.dart | 16 +- .../reports/npe_report_from_tarball.dart | 2 +- .../reports/npe_report_new_location.dart | 2 +- .../widgets/in_situ_step_4_summary.dart | 53 +- .../settings/telegram_alert_settings.dart | 16 +- .../marine_in_situ_sampling_service.dart | 9 +- lib/services/marine_npe_report_service.dart | 2 +- lib/services/retry_service.dart | 598 +++++++++------ .../river_investigative_sampling_service.dart | 75 +- ...ver_manual_triennial_sampling_service.dart | 42 +- lib/services/settings_service.dart | 40 +- 15 files changed, 1047 insertions(+), 870 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index b12c870..0144655 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,6 @@ 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/river_in_situ_sampling_service.dart'; import 'package:environment_monitoring_app/services/river_manual_triennial_sampling_service.dart'; -// *** ADDED: Import River Investigative Sampling Service *** import 'package:environment_monitoring_app/services/river_investigative_sampling_service.dart'; import 'package:environment_monitoring_app/services/air_sampling_service.dart'; import 'package:environment_monitoring_app/services/telegram_service.dart'; @@ -21,7 +20,7 @@ import 'package:environment_monitoring_app/services/retry_service.dart'; import 'package:environment_monitoring_app/services/marine_in_situ_sampling_service.dart'; import 'package:environment_monitoring_app/services/marine_investigative_sampling_service.dart'; import 'package:environment_monitoring_app/services/marine_npe_report_service.dart'; -import 'package:environment_monitoring_app/services/marine_tarball_sampling_service.dart'; // Ensure this import is present +import 'package:environment_monitoring_app/services/marine_tarball_sampling_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'; @@ -38,14 +37,13 @@ import 'package:environment_monitoring_app/home_page.dart'; import 'package:environment_monitoring_app/screens/profile.dart'; import 'package:environment_monitoring_app/screens/settings.dart'; -// --- START: New Settings Screen Imports --- +// Settings Screen Imports import 'package:environment_monitoring_app/screens/settings/submission_preferences_settings.dart'; import 'package:environment_monitoring_app/screens/settings/telegram_alert_settings.dart'; import 'package:environment_monitoring_app/screens/settings/api_ftp_configurations_settings.dart'; import 'package:environment_monitoring_app/screens/settings/parameter_limits_settings.dart'; import 'package:environment_monitoring_app/screens/settings/air_clients_settings.dart'; import 'package:environment_monitoring_app/screens/settings/station_info_settings.dart'; -// --- END: New Settings Screen Imports --- // Department Home Pages import 'package:environment_monitoring_app/screens/air/air_home_page.dart'; @@ -80,14 +78,9 @@ import 'package:environment_monitoring_app/screens/river/continuous/overview.dar import 'package:environment_monitoring_app/screens/river/continuous/entry.dart' as riverContinuousEntry; import 'package:environment_monitoring_app/screens/river/continuous/report.dart' as riverContinuousReport; import 'package:environment_monitoring_app/screens/river/investigative/river_investigative_info_centre_document.dart'; -// *** ADDED: Import River Investigative Manual Sampling Screen *** import 'package:environment_monitoring_app/screens/river/investigative/river_investigative_manual_sampling.dart' as riverInvestigativeManualSampling; -// *** START: ADDED NEW RIVER INVESTIGATIVE IMPORTS *** -import 'package:environment_monitoring_app/screens/river/investigative/river_investigative_data_status_log.dart' -as riverInvestigativeDataStatusLog; -import 'package:environment_monitoring_app/screens/river/investigative/river_investigative_image_request.dart' -as riverInvestigativeImageRequest; -// *** END: ADDED NEW RIVER INVESTIGATIVE IMPORTS *** +import 'package:environment_monitoring_app/screens/river/investigative/river_investigative_data_status_log.dart' as riverInvestigativeDataStatusLog; +import 'package:environment_monitoring_app/screens/river/investigative/river_investigative_image_request.dart' as riverInvestigativeImageRequest; import 'package:environment_monitoring_app/screens/river/investigative/overview.dart' as riverInvestigativeOverview; import 'package:environment_monitoring_app/screens/river/investigative/entry.dart' as riverInvestigativeEntry; import 'package:environment_monitoring_app/screens/river/investigative/report.dart' as riverInvestigativeReport; @@ -98,32 +91,20 @@ import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_p import 'package:environment_monitoring_app/screens/marine/manual/in_situ_sampling.dart' as marineManualInSituSampling; import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_report.dart' as marineManualReport; import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_npe_report_hub.dart'; -import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_pre_departure_checklist_screen.dart' -as marineManualPreDepartureChecklist; -import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_sonde_calibration_screen.dart' -as marineManualSondeCalibration; -import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_equipment_maintenance_screen.dart' -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/reports/marine_manual_pre_departure_checklist_screen.dart' as marineManualPreDepartureChecklist; +import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_sonde_calibration_screen.dart' as marineManualSondeCalibration; +import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_equipment_maintenance_screen.dart' as marineManualEquipmentMaintenance; +import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_data_status_log.dart' as marineManualDataStatusLog; +import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_report_status_log.dart' as marineManualReportStatusLog; 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; import 'package:environment_monitoring_app/screens/marine/continuous/entry.dart' as marineContinuousEntry; import 'package:environment_monitoring_app/screens/marine/continuous/report.dart' as marineContinuousReport; import 'package:environment_monitoring_app/screens/marine/investigative/marine_investigative_info_centre_document.dart'; -import 'package:environment_monitoring_app/screens/marine/investigative/marine_investigative_manual_sampling.dart' -as marineInvestigativeManualSampling; -// *** START: ADDED NEW MARINE INVESTIGATIVE IMPORTS *** -import 'package:environment_monitoring_app/screens/marine/investigative/marine_investigative_data_status_log.dart' -as marineInvestigativeDataStatusLog; -import 'package:environment_monitoring_app/screens/marine/investigative/marine_investigative_image_request.dart' -as marineInvestigativeImageRequest; -// *** END: ADDED NEW MARINE INVESTIGATIVE IMPORTS *** +import 'package:environment_monitoring_app/screens/marine/investigative/marine_investigative_manual_sampling.dart' as marineInvestigativeManualSampling; +import 'package:environment_monitoring_app/screens/marine/investigative/marine_investigative_data_status_log.dart' as marineInvestigativeDataStatusLog; +import 'package:environment_monitoring_app/screens/marine/investigative/marine_investigative_image_request.dart' as marineInvestigativeImageRequest; import 'package:environment_monitoring_app/screens/marine/investigative/overview.dart' as marineInvestigativeOverview; import 'package:environment_monitoring_app/screens/marine/investigative/entry.dart' as marineInvestigativeEntry; import 'package:environment_monitoring_app/screens/marine/investigative/report.dart' as marineInvestigativeReport; @@ -141,15 +122,21 @@ void main() async { final TelegramService telegramService = TelegramService(); final ApiService apiService = ApiService(telegramService: telegramService); final RetryService retryService = RetryService(); + + // Sampling Services final MarineInSituSamplingService marineInSituService = MarineInSituSamplingService(telegramService); final RiverInSituSamplingService riverInSituService = RiverInSituSamplingService(telegramService); - final MarineInvestigativeSamplingService marineInvestigativeService = - MarineInvestigativeSamplingService(telegramService); - // *** ADDED: Create instance of RiverInvestigativeSamplingService *** - final RiverInvestigativeSamplingService riverInvestigativeService = - RiverInvestigativeSamplingService(telegramService); + final MarineInvestigativeSamplingService marineInvestigativeService = MarineInvestigativeSamplingService(telegramService); + final RiverInvestigativeSamplingService riverInvestigativeService = RiverInvestigativeSamplingService(telegramService); final MarineTarballSamplingService marineTarballService = MarineTarballSamplingService(telegramService); + // --- START: Instantiate Marine Report Services --- + final MarineNpeReportService marineNpeService = MarineNpeReportService(telegramService); + final MarineManualPreDepartureService marinePreDepartureService = MarineManualPreDepartureService(apiService); + final MarineManualSondeCalibrationService marineSondeCalibrationService = MarineManualSondeCalibrationService(apiService); + final MarineManualEquipmentMaintenanceService marineEquipmentMaintenanceService = MarineManualEquipmentMaintenanceService(apiService); + // --- END: Instantiate Marine Report Services --- + telegramService.setApiService(apiService); // The AuthProvider needs to be created here so it can be passed to the retry service. @@ -161,13 +148,20 @@ void main() async { ); // Initialize the retry service with all its dependencies. - // *** MODIFIED: Added riverInvestigativeService dependency (and marineTarballService from previous request) *** retryService.initialize( marineInSituService: marineInSituService, riverInSituService: riverInSituService, marineInvestigativeService: marineInvestigativeService, - riverInvestigativeService: riverInvestigativeService, // <-- Added this line + riverInvestigativeService: riverInvestigativeService, marineTarballService: marineTarballService, + + // --- START: Pass the new services to initialize --- + marineNpeService: marineNpeService, + marinePreDepartureService: marinePreDepartureService, + marineSondeCalibrationService: marineSondeCalibrationService, + marineEquipmentMaintenanceService: marineEquipmentMaintenanceService, + // --- END: Pass the new services --- + authProvider: authProvider, ); @@ -182,24 +176,23 @@ void main() async { Provider(create: (_) => telegramService), Provider(create: (_) => LocalStorageService()), Provider.value(value: retryService), + + // Sampling Services Provider.value(value: marineInSituService), Provider.value(value: marineInvestigativeService), Provider.value(value: riverInSituService), - // *** ADDED: Provider for River Investigative Service *** - Provider.value(value: riverInvestigativeService), // Use Provider.value + Provider.value(value: riverInvestigativeService), + Provider.value(value: marineTarballService), + + // Report Services (Use Provider.value since they are already instantiated) + Provider.value(value: marineNpeService), + Provider.value(value: marinePreDepartureService), + Provider.value(value: marineSondeCalibrationService), + Provider.value(value: marineEquipmentMaintenanceService), + + // Other Independent Services Provider(create: (context) => RiverManualTriennialSamplingService(telegramService)), Provider(create: (context) => AirSamplingService(databaseHelper, telegramService)), - Provider.value(value: marineTarballService), // Use Provider.value - Provider(create: (context) => MarineNpeReportService(Provider.of(context, listen: false))), - Provider( - create: (context) => - MarineManualPreDepartureService(Provider.of(context, listen: false))), - Provider( - create: (context) => - MarineManualSondeCalibrationService(Provider.of(context, listen: false))), - Provider( - create: (context) => - MarineManualEquipmentMaintenanceService(Provider.of(context, listen: false))), ], child: const RootApp(), ), @@ -232,67 +225,49 @@ class RootApp extends StatefulWidget { } class _RootAppState extends State { - // --- START: MODIFICATION FOR HOURLY SYNC --- Timer? _configSyncTimer; - // --- END: MODIFICATION --- @override void initState() { super.initState(); _initializeConnectivityListener(); _performInitialSessionCheck(); - // --- START: MODIFICATION FOR HOURLY SYNC --- - _initializePeriodicSync(); // Start the hourly sync timer - // --- END: MODIFICATION --- + _initializePeriodicSync(); } - // --- START: MODIFICATION FOR HOURLY SYNC --- @override void dispose() { - _configSyncTimer?.cancel(); // Cancel the timer when the app closes + _configSyncTimer?.cancel(); super.dispose(); } - // --- END: MODIFICATION --- - - /// Initial check when app loads to see if we need to transition from offline to online. void _performInitialSessionCheck() async { - // Wait a moment for providers to be fully available. await Future.delayed(const Duration(milliseconds: 100)); if (mounted) { final authProvider = Provider.of(context, listen: false); - - // Perform proactive token refresh on app start await authProvider.proactiveTokenRefresh(); - - // First, try to transition from an offline placeholder token to an online one. final didTransition = await authProvider.checkAndTransitionToOnlineSession(); - // If no transition happened (i.e., we were already supposed to be online), validate the session. if (!didTransition) { authProvider.validateAndRefreshSession(); } } } - /// Listens for connectivity changes to trigger auto-relogin or queue processing. void _initializeConnectivityListener() { Connectivity().onConnectivityChanged.listen((List results) { if (!results.contains(ConnectivityResult.none)) { debugPrint("[Main] Internet connection detected."); if (mounted) { - // Access services from provider context final authProvider = Provider.of(context, listen: false); final telegramService = Provider.of(context, listen: false); final retryService = Provider.of(context, listen: false); - // When connection is restored, always try to transition/validate the session. authProvider.checkAndTransitionToOnlineSession().then((didTransition) { if (!didTransition) { authProvider.validateAndRefreshSession(); } }); - // Process queues telegramService.processAlertQueue(); retryService.processRetryQueue(); } @@ -302,22 +277,13 @@ class _RootAppState extends State { }); } - // --- START: MODIFICATION FOR HOURLY SYNC --- - /// Initializes a recurring timer to sync data periodically. void _initializePeriodicSync() { - // Start a timer for 1 hour (as requested). You can change this duration. _configSyncTimer = Timer.periodic(const Duration(hours: 1), (timer) { debugPrint("[Main] Periodic 1-hour sync triggered."); - - // Use 'context.read' for a safe, one-time read inside a timer if (mounted) { final authProvider = context.read(); - - // Only sync if the user is logged in and the session isn't expired if (authProvider.isLoggedIn && !authProvider.isSessionExpired) { debugPrint("[Main] User is logged in. Starting periodic data sync..."); - - // Run syncAllData but don't block. Catch errors to prevent the timer from stopping. authProvider.syncAllData().catchError((e) { debugPrint("[Main] Error during periodic 1-hour sync: $e"); }); @@ -327,7 +293,6 @@ class _RootAppState extends State { } }); } - // --- END: MODIFICATION --- @override Widget build(BuildContext context) { @@ -348,7 +313,6 @@ class _RootAppState extends State { debugShowCheckedModeBanner: false, home: homeWidget, onGenerateRoute: (settings) { - // Keep existing onGenerateRoute logic for Tarball if (settings.name == '/marine/manual/tarball/step2') { final args = settings.arguments as TarballSamplingData; return MaterialPageRoute(builder: (context) { @@ -366,126 +330,74 @@ class _RootAppState extends State { return const marineManualDataStatusLog.MarineManualDataStatusLog(); }); } - // Add other potential dynamic routes here if necessary - return null; // Let routes map handle named routes + return null; }, routes: { - // Auth Routes '/register': (context) => const RegisterScreen(), '/forgot-password': (context) => ForgotPasswordScreen(), '/logout': (context) => const LogoutScreen(), '/home': (context) => const HomePage(), '/profile': (context) => const ProfileScreen(), '/settings': (context) => const SettingsScreen(), - - // --- START: New Settings Routes (const removed) --- - '/settings/submission-prefs': (context) => - SubmissionPreferencesSettingsScreen(), - '/settings/telegram-alerts': (context) => - TelegramAlertSettingsScreen(), - '/settings/api-ftp-configs': (context) => - ApiFtpConfigurationsSettingsScreen(), - '/settings/parameter-limits': (context) => - ParameterLimitsSettingsScreen(), - '/settings/air-clients': (context) => - AirClientsSettingsScreen(), - '/settings/station-info': (context) => - StationInfoSettingsScreen(), - // --- END: New Settings Routes --- - - // Department Home Pages + '/settings/submission-prefs': (context) => SubmissionPreferencesSettingsScreen(), + '/settings/telegram-alerts': (context) => TelegramAlertSettingsScreen(), + '/settings/api-ftp-configs': (context) => ApiFtpConfigurationsSettingsScreen(), + '/settings/parameter-limits': (context) => ParameterLimitsSettingsScreen(), + '/settings/air-clients': (context) => AirClientsSettingsScreen(), + '/settings/station-info': (context) => StationInfoSettingsScreen(), '/air/home': (context) => const AirHomePage(), '/river/home': (context) => const RiverHomePage(), '/marine/home': (context) => const MarineHomePage(), - - // Air Manual '/air/manual/info': (context) => const AirManualInfoCentreDocument(), '/air/manual/installation': (context) => const AirManualInstallationScreen(), '/air/manual/collection': (context) => const AirManualCollectionScreen(), '/air/manual/report': (context) => airManualReport.AirManualReport(), '/air/manual/data-log': (context) => airManualDataStatusLog.AirManualDataStatusLog(), '/air/manual/image-request': (context) => airManualImageRequest.AirManualImageRequest(), - - // Air Continuous '/air/continuous/info': (context) => const AirContinuousInfoCentreDocument(), '/air/continuous/overview': (context) => airContinuousOverview.OverviewScreen(), '/air/continuous/entry': (context) => airContinuousEntry.EntryScreen(), '/air/continuous/report': (context) => airContinuousReport.ReportScreen(), - - // Air Investigative '/air/investigative/info': (context) => const AirInvestigativeInfoCentreDocument(), '/air/investigative/overview': (context) => airInvestigativeOverview.OverviewScreen(), '/air/investigative/entry': (context) => airInvestigativeEntry.EntryScreen(), '/air/investigative/report': (context) => airInvestigativeReport.ReportScreen(), - - // River Manual '/river/manual/info': (context) => const RiverManualInfoCentreDocument(), '/river/manual/in-situ': (context) => riverManualInSituSampling.RiverInSituSamplingScreen(), '/river/manual/report': (context) => riverManualReport.RiverManualReport(), - '/river/manual/triennial': (context) => - riverManualTriennialSampling.RiverManualTriennialSamplingScreen(), + '/river/manual/triennial': (context) => riverManualTriennialSampling.RiverManualTriennialSamplingScreen(), '/river/manual/data-log': (context) => riverManualDataStatusLog.RiverManualDataStatusLog(), '/river/manual/image-request': (context) => riverManualImageRequest.RiverManualImageRequest(), - - // River Continuous '/river/continuous/info': (context) => const RiverContinuousInfoCentreDocument(), '/river/continuous/overview': (context) => riverContinuousOverview.OverviewScreen(), '/river/continuous/entry': (context) => riverContinuousEntry.EntryScreen(), '/river/continuous/report': (context) => riverContinuousReport.ReportScreen(), - - // River Investigative '/river/investigative/info': (context) => const RiverInvestigativeInfoCentreDocument(), - // *** ADDED: Route for River Investigative Manual Sampling *** - '/river/investigative/manual-sampling': (context) => - riverInvestigativeManualSampling.RiverInvestigativeManualSamplingScreen(), - // *** START: ADDED NEW RIVER INVESTIGATIVE ROUTES *** - '/river/investigative/data-log': (context) => - const riverInvestigativeDataStatusLog.RiverInvestigativeDataStatusLog(), - '/river/investigative/image-request': (context) => - const riverInvestigativeImageRequest.RiverInvestigativeImageRequest(), - // *** END: ADDED NEW RIVER INVESTIGATIVE ROUTES *** - '/river/investigative/overview': (context) => - riverInvestigativeOverview.OverviewScreen(), // Keep placeholder/future routes - '/river/investigative/entry': (context) => - riverInvestigativeEntry.EntryScreen(), // Keep placeholder/future routes - '/river/investigative/report': (context) => - riverInvestigativeReport.ReportScreen(), // Keep placeholder/future routes - - // Marine Manual + '/river/investigative/manual-sampling': (context) => riverInvestigativeManualSampling.RiverInvestigativeManualSamplingScreen(), + '/river/investigative/data-log': (context) => const riverInvestigativeDataStatusLog.RiverInvestigativeDataStatusLog(), + '/river/investigative/image-request': (context) => const riverInvestigativeImageRequest.RiverInvestigativeImageRequest(), + '/river/investigative/overview': (context) => riverInvestigativeOverview.OverviewScreen(), + '/river/investigative/entry': (context) => riverInvestigativeEntry.EntryScreen(), + '/river/investigative/report': (context) => riverInvestigativeReport.ReportScreen(), '/marine/manual/info': (context) => marineManualInfoCentreDocument.MarineInfoCentreDocument(), '/marine/manual/pre-sampling': (context) => marineManualPreSampling.MarineManualPreSampling(), '/marine/manual/in-situ': (context) => marineManualInSituSampling.MarineInSituSampling(), '/marine/manual/tarball': (context) => const TarballSamplingStep1(), '/marine/manual/report': (context) => const marineManualReport.MarineManualReportHomePage(), '/marine/manual/report/npe': (context) => const MarineManualNPEReportHub(), - '/marine/manual/report/pre-departure': (context) => - const marineManualPreDepartureChecklist.MarineManualPreDepartureChecklistScreen(), - '/marine/manual/report/calibration': (context) => - const marineManualSondeCalibration.MarineManualSondeCalibrationScreen(), - '/marine/manual/report/maintenance': (context) => - const marineManualEquipmentMaintenance.MarineManualEquipmentMaintenanceScreen(), + '/marine/manual/report/pre-departure': (context) => const marineManualPreDepartureChecklist.MarineManualPreDepartureChecklistScreen(), + '/marine/manual/report/calibration': (context) => const marineManualSondeCalibration.MarineManualSondeCalibrationScreen(), + '/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/manual/report-log': (context) => const marineManualReportStatusLog.MarineManualReportStatusLog(), '/marine/continuous/info': (context) => const MarineContinuousInfoCentreDocument(), '/marine/continuous/overview': (context) => marineContinuousOverview.OverviewScreen(), '/marine/continuous/entry': (context) => marineContinuousEntry.EntryScreen(), '/marine/continuous/report': (context) => marineContinuousReport.ReportScreen(), - - // Marine Investigative '/marine/investigative/info': (context) => const MarineInvestigativeInfoCentreDocument(), - '/marine/investigative/manual-sampling': (context) => - marineInvestigativeManualSampling.MarineInvestigativeManualSampling(), - // *** START: ADDED NEW MARINE INVESTIGATIVE ROUTES *** - '/marine/investigative/data-log': (context) => - const marineInvestigativeDataStatusLog.MarineInvestigativeDataStatusLog(), - '/marine/investigative/image-request': (context) => - const marineInvestigativeImageRequest.MarineInvestigativeImageRequestScreen(), - // *** END: ADDED NEW MARINE INVESTIGATIVE ROUTES *** + '/marine/investigative/manual-sampling': (context) => marineInvestigativeManualSampling.MarineInvestigativeManualSampling(), + '/marine/investigative/data-log': (context) => const marineInvestigativeDataStatusLog.MarineInvestigativeDataStatusLog(), + '/marine/investigative/image-request': (context) => const marineInvestigativeImageRequest.MarineInvestigativeImageRequestScreen(), '/marine/investigative/overview': (context) => marineInvestigativeOverview.OverviewScreen(), '/marine/investigative/entry': (context) => marineInvestigativeEntry.EntryScreen(), '/marine/investigative/report': (context) => marineInvestigativeReport.ReportScreen(), @@ -506,50 +418,33 @@ class SessionAwareWrapper extends StatefulWidget { class _SessionAwareWrapperState extends State { bool _isDialogShowing = false; - // --- MODIFICATION START --- - // 1. Create a variable to hold the AuthProvider instance. late AuthProvider _authProvider; - // --- MODIFICATION END --- @override void didChangeDependencies() { super.didChangeDependencies(); - // --- MODIFICATION START --- - // 2. Get the provider reference here and add the listener. _authProvider = Provider.of(context); _authProvider.addListener(_handleSessionExpired); - // --- MODIFICATION END --- - - // Call initial check here if needed, or rely on RootApp's check. _checkAndShowDialogIfNeeded(_authProvider.isSessionExpired); } @override void dispose() { - // --- MODIFICATION START --- - // 3. Use the saved reference to remove the listener. This is safe. _authProvider.removeListener(_handleSessionExpired); - // --- MODIFICATION END --- super.dispose(); } void _handleSessionExpired() { - // --- MODIFICATION START --- - // 4. Use the saved _authProvider reference. _checkAndShowDialogIfNeeded(_authProvider.isSessionExpired); - // --- MODIFICATION END --- } void _checkAndShowDialogIfNeeded(bool isExpired) { if (isExpired && !_isDialogShowing && mounted) { WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted && !_isDialogShowing) { // Double check mounted and flag + if (mounted && !_isDialogShowing) { _showSessionExpiredDialog(); } }); - } else if (!isExpired && _isDialogShowing && mounted) { - // If session becomes valid again and dialog is showing, maybe dismiss it? - // Or rely on user action. For now, we only trigger ON expiry. } } @@ -561,7 +456,6 @@ class _SessionAwareWrapperState extends State { context: context, barrierDismissible: false, builder: (BuildContext dialogContext) { - // Use the state's _authProvider reference, which is safe. return AlertDialog( title: const Text("Session Expired"), content: const Text( @@ -571,25 +465,19 @@ class _SessionAwareWrapperState extends State { child: const Text("Continue Offline"), onPressed: () { Navigator.of(dialogContext).pop(); - // Optionally: _authProvider.clearSessionExpiredFlag(); // If needed }, ), ElevatedButton( child: const Text("Login Now"), onPressed: () { - // --- MODIFICATION START --- - // 5. Use the saved reference to log out. _authProvider.logout(); - // --- MODIFICATION END --- - Navigator.of(dialogContext).pop(); // Close dialog first - // RootApp builder will handle navigation to LoginScreen + Navigator.of(dialogContext).pop(); }, ), ], ); }, ); - // Reset flag after dialog is dismissed if (mounted) { setState(() => _isDialogShowing = false); } @@ -612,7 +500,7 @@ class SplashScreen extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( - 'assets/icon4.png', // Ensure this asset exists + 'assets/icon4.png', height: 360, width: 360, ), diff --git a/lib/models/marine_manual_npe_report_data.dart b/lib/models/marine_manual_npe_report_data.dart index 6b85d44..ea0389b 100644 --- a/lib/models/marine_manual_npe_report_data.dart +++ b/lib/models/marine_manual_npe_report_data.dart @@ -11,6 +11,9 @@ class MarineManualNpeReportData { String? eventDate; String? eventTime; + // --- Source Origin (Kept for internal tracking, but not displayed) --- + String? sourceOrigin; + // --- Location Info --- String? locationDescription; // For new locations String? stateName; // For new locations or tarball stations @@ -52,10 +55,9 @@ class MarineManualNpeReportData { String? image3Remark; String? image4Remark; - // --- START: Added Tarball Classification Fields --- + // --- Tarball Classification Fields --- int? tarballClassificationId; Map? selectedTarballClassification; - // --- END: Added Tarball Classification Fields --- // --- Submission Status --- String? submissionStatus; @@ -71,6 +73,7 @@ class MarineManualNpeReportData { 'firstSamplerUserId': firstSamplerUserId, 'eventDate': eventDate, 'eventTime': eventTime, + 'sourceOrigin': sourceOrigin, 'locationDescription': locationDescription, 'stateName': stateName, 'selectedStation': selectedStation, @@ -89,10 +92,8 @@ class MarineManualNpeReportData { 'image2Remark': image2Remark, 'image3Remark': image3Remark, 'image4Remark': image4Remark, - // --- Added Fields --- 'tarballClassificationId': tarballClassificationId, 'selectedTarballClassification': selectedTarballClassification, - // --- 'submissionStatus': submissionStatus, 'submissionMessage': submissionMessage, 'reportId': reportId, @@ -111,6 +112,9 @@ class MarineManualNpeReportData { add('npe_date', eventDate); add('npe_time', eventTime); add('first_sampler_user_id', firstSamplerUserId); + + // add('npe_source_origin', sourceOrigin); // Disabled to prevent SQL error + if (selectedStation != null) { add('station_id', selectedStation?['station_id'] ?? selectedStation?['tbl_station_id']); add('station_code', selectedStation?['man_station_code'] ?? selectedStation?['tbl_station_code']); @@ -143,9 +147,7 @@ class MarineManualNpeReportData { add('npe_image_3_remarks', image3Remark); add('npe_image_4_remarks', image4Remark); - // --- Added Fields --- add('classification_id', tarballClassificationId); - // --- return map; } @@ -162,16 +164,24 @@ class MarineManualNpeReportData { /// Generates the Telegram alert message for this NPE report. String generateTelegramAlertMessage() { - final locationDesc = selectedStation != null - ? '${selectedStation!['man_station_name'] ?? selectedStation!['tbl_station_name']}' - : locationDescription ?? 'A custom location'; + String locationDesc; + + // --- START: MODIFIED to include Station Code + Name --- + if (selectedStation != null) { + final code = selectedStation!['man_station_code'] ?? selectedStation!['tbl_station_code'] ?? 'N/A'; + final name = selectedStation!['man_station_name'] ?? selectedStation!['tbl_station_name'] ?? 'N/A'; + locationDesc = '$code - $name'; + } else { + locationDesc = locationDescription ?? 'A custom location'; + } + // --- END: MODIFIED --- final buffer = StringBuffer() ..writeln('🚨 *Notification of Pollution Event (NPE) Submitted:*') ..writeln() ..writeln('*Location:* $locationDesc') ..writeln('*Event Date:* $eventDate $eventTime') - ..writeln('*Submitted by:* $firstSamplerName') + ..writeln('*Submitted by:* ${firstSamplerName ?? "N/A"}') ..writeln('*Status of Submission:* Successful'); final observations = fieldObservations.entries @@ -195,13 +205,11 @@ class MarineManualNpeReportData { ..writeln('*Possible Source:* $possibleSource'); } - // --- Added Tarball Classification to Telegram message --- if (selectedTarballClassification != null) { buffer ..writeln() ..writeln('*Tarball Classification:* ${selectedTarballClassification!['classification_name']}'); } - // --- final remarks = [ if (image1Remark != null && image1Remark!.isNotEmpty) '*Fig 1:* $image1Remark', diff --git a/lib/models/river_manual_triennial_sampling_data.dart b/lib/models/river_manual_triennial_sampling_data.dart index ff5383f..3443466 100644 --- a/lib/models/river_manual_triennial_sampling_data.dart +++ b/lib/models/river_manual_triennial_sampling_data.dart @@ -334,4 +334,61 @@ class RiverManualTriennialSamplingData { //data.removeWhere((key, value) => value == null); return jsonEncode(data); } + + /// Creates a JSON object for basic form info, mimicking 'river_triennial_basic_form.json'. + String toBasicFormJson() { + final data = { + // --- START FIX: Map model properties to correct form keys --- + 'tech_name': firstSamplerName, + 'sampler_2ndname': secondSampler?['first_name'], + 'sample_date': samplingDate, + 'sample_time': samplingTime, + 'sampling_type': samplingType, + 'sample_state': selectedStateName, + 'station_id': selectedStation?['sampling_station_code'], + 'station_latitude': stationLatitude, + 'station_longitude': stationLongitude, + 'latitude': currentLatitude, // Current location lat + 'longitude': currentLongitude, // Current location lon + 'sample_id': sampleIdCode, + // --- END FIX --- + }; + data.removeWhere((key, value) => value == null); + return jsonEncode(data); + } + + /// Creates a JSON object for sensor readings, mimicking 'river_sampling_reading.json'. + String toReadingJson() { + final data = { + // --- START FIX: Map model properties to correct reading keys --- + 'do_mgl': oxygenConcentration == -999.0 ? null : oxygenConcentration, + 'do_sat': oxygenSaturation == -999.0 ? null : oxygenSaturation, + 'ph': ph == -999.0 ? null : ph, + 'salinity': salinity == -999.0 ? null : salinity, + 'temperature': temperature == -999.0 ? null : temperature, + 'turbidity': turbidity == -999.0 ? null : turbidity, + 'tds': tds == -999.0 ? null : tds, + 'electric_conductivity': electricalConductivity == -999.0 ? null : electricalConductivity, + 'ammonia': ammonia == -999.0 ? null : ammonia, + 'flowrate': flowrateValue, + 'date_sampling_reading': dataCaptureDate, // Use data capture date/time + 'time_sampling_reading': dataCaptureTime, // Use data capture date/time + // --- END FIX --- + }; + data.removeWhere((key, value) => value == null); + return jsonEncode(data); + } + + /// Creates a JSON object for manual info, mimicking 'river_manual_info.json'. + String toManualInfoJson() { + final data = { + // --- START FIX: Map model properties to correct manual info keys --- + 'weather': weather, + 'remarks_event': eventRemarks, + 'remarks_lab': labRemarks, + // --- END FIX --- + }; + data.removeWhere((key, value) => value == null); + return jsonEncode(data); + } } \ 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 index 5ec7199..c8ed0c4 100644 --- a/lib/screens/marine/manual/marine_manual_report_status_log.dart +++ b/lib/screens/marine/manual/marine_manual_report_status_log.dart @@ -36,7 +36,7 @@ class SubmissionLogEntry { final String type; final String title; final String stationCode; - final String senderName; // <-- ADDED + final String senderName; final DateTime submissionDateTime; final String? reportId; final String status; @@ -51,7 +51,7 @@ class SubmissionLogEntry { required this.type, required this.title, required this.stationCode, - required this.senderName, // <-- ADDED + required this.senderName, required this.submissionDateTime, this.reportId, required this.status, @@ -97,7 +97,8 @@ class _MarineManualReportStatusLogState // --- START: COPIED FROM SCREEN FILE --- // This is the "single source of truth" for categories final Map> _checklistSections = { - 'INTERNAL - IN-SITU SAMPLING': [ // Section title matches PDF + 'INTERNAL - IN-SITU SAMPLING': [ + // Section title matches PDF 'Marine manual Standard Operation Procedure (SOP)', // Item text matches PDF 'Back-up Sampling Sheet & Chain of Custody form', // Item text matches PDF 'Calibration worksheet', // Item text matches PDF @@ -125,13 +126,15 @@ class _MarineManualReportStatusLogState 'Raincoat/Poncho', // Item text matches PDF 'Ice packets', // Item text matches PDF ], - 'INTERNAL-TARBALL SAMPLING': [ // Section title matches PDF + 'INTERNAL-TARBALL SAMPLING': [ + // Section title matches PDF 'Measuring tape (100 meter)', // Item text matches PDF 'Steel raking', // Item text matches PDF 'Aluminum foil', // Item text matches PDF 'Zipper bags', // Item text matches PDF ], - 'EXTERNAL - LABORATORY': [ // Section title matches PDF + 'EXTERNAL - LABORATORY': [ + // Section title matches PDF 'Sufficient sets of cooler box and sampling bottles with label', // Item text matches PDF 'Field duplicate sampling bottles (if any)', // Item text matches PDF 'Blank samples sampling bottles (if any)', // Item text matches PDF @@ -152,14 +155,18 @@ class _MarineManualReportStatusLogState 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 + _localStorageService = + Provider.of(context, listen: false); + _npeReportService = + Provider.of(context, listen: false); + _preDepartureService = Provider.of(context, + listen: false); + _sondeCalibrationService = Provider.of( + context, + listen: false); _equipmentMaintenanceService = - Provider.of(context, listen: false); // Added listen: false + Provider.of(context, + listen: false); // Load logs after services are initialized if (_isLoading) { @@ -185,9 +192,7 @@ class _MarineManualReportStatusLogState 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 = []; @@ -199,9 +204,7 @@ class _MarineManualReportStatusLogState type: 'Pre-Departure Checklist', title: 'Pre-Departure Checklist', stationCode: 'N/A', - // --- START: MODIFIED --- senderName: (log['reporterName'] as String?) ?? 'Unknown User', - // --- END: MODIFIED --- submissionDateTime: DateTime.tryParse(dateStr) ?? DateTime.fromMillisecondsSinceEpoch(0), reportId: log['reportId']?.toString(), @@ -215,18 +218,12 @@ class _MarineManualReportStatusLogState // 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 --- + title: 'Sonde Calibration', 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(), @@ -240,16 +237,12 @@ class _MarineManualReportStatusLogState // 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(), @@ -277,9 +270,7 @@ class _MarineManualReportStatusLogState 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 --- + senderName: (log['firstSamplerName'] as String?) ?? 'Unknown User', submissionDateTime: DateTime.tryParse('$dateStr $timeStr') ?? DateTime.fromMillisecondsSinceEpoch(0), reportId: log['reportId']?.toString(), @@ -288,14 +279,15 @@ class _MarineManualReportStatusLogState rawData: log, serverName: log['serverConfigName'] ?? 'Unknown Server', apiStatusRaw: log['api_status'], - ftpStatusRaw: log['ftp_status'], // NPE has FTP + ftpStatusRaw: log['ftp_status'], )); } // Sort logs by date (descending) tempPreSampling .sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime)); - tempReport.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime)); + tempReport + .sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime)); if (mounted) { setState(() { @@ -328,7 +320,7 @@ class _MarineManualReportStatusLogState log.stationCode.toLowerCase().contains(query) || log.serverName.toLowerCase().contains(query) || log.type.toLowerCase().contains(query) || - log.senderName.toLowerCase().contains(query) || // <-- ADDED + log.senderName.toLowerCase().contains(query) || (log.reportId?.toLowerCase() ?? '').contains(query); } @@ -387,23 +379,25 @@ class _MarineManualReportStatusLogState 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.ph7Mv = (logData['ph_7_mv'] as num?)?.toDouble(); + data.ph7Before = (logData['ph_7_before'] as num?)?.toDouble(); + data.ph7After = (logData['ph_7_after'] as num?)?.toDouble(); + data.ph10Mv = (logData['ph_10_mv'] as num?)?.toDouble(); + data.ph10Before = (logData['ph_10_before'] as num?)?.toDouble(); + data.ph10After = (logData['ph_10_after'] as num?)?.toDouble(); + data.condBefore = (logData['cond_before'] as num?)?.toDouble(); + data.condAfter = (logData['cond_after'] as num?)?.toDouble(); + data.doBefore = (logData['do_before'] as num?)?.toDouble(); + data.doAfter = (logData['do_after'] as num?)?.toDouble(); + data.turbidity0Before = + (logData['turbidity_0_before'] as num?)?.toDouble(); + data.turbidity0After = + (logData['turbidity_0_after'] as num?)?.toDouble(); data.turbidity124Before = - (logData['turbidity_124_before'] as num?)?.toDouble(); // Fixed key + (logData['turbidity_124_before'] as num?)?.toDouble(); data.turbidity124After = - (logData['turbidity_124_after'] as num?)?.toDouble(); // Fixed key - data.calibrationStatus = logData['calibration_status']; // Fixed key + (logData['turbidity_124_after'] as num?)?.toDouble(); + data.calibrationStatus = logData['calibration_status']; data.remarks = logData['remarks']; result = await _sondeCalibrationService.submitCalibration( @@ -461,9 +455,7 @@ class _MarineManualReportStatusLogState ); } - // *** START: Fixed method name *** result = await _equipmentMaintenanceService.submitMaintenanceReport( - // *** END: Fixed method name *** data: data, authProvider: authProvider, appSettings: appSettings, @@ -483,7 +475,8 @@ class _MarineManualReportStatusLogState data.selectedStation = logData['selectedStation']; data.latitude = logData['latitude']; data.longitude = logData['longitude']; - data.oxygenSaturation = (logData['oxygenSaturation'] as num?)?.toDouble(); + data.oxygenSaturation = + (logData['oxygenSaturation'] as num?)?.toDouble(); data.electricalConductivity = (logData['electricalConductivity'] as num?)?.toDouble(); data.oxygenConcentration = @@ -512,13 +505,11 @@ class _MarineManualReportStatusLogState 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: @@ -528,8 +519,8 @@ class _MarineManualReportStatusLogState if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - result['message'] ?? 'Resubmission process completed.')), + content: + Text(result['message'] ?? 'Resubmission process completed.')), ); } } catch (e) { @@ -580,9 +571,7 @@ class _MarineManualReportStatusLogState } /// Builds a collapsible card for a category of logs. - Widget _buildCategorySection( - String category, - List logs, + Widget _buildCategorySection(String category, List logs, TextEditingController searchController) { return Card( margin: const EdgeInsets.symmetric(vertical: 8.0), @@ -621,7 +610,8 @@ class _MarineManualReportStatusLogState const Padding( padding: EdgeInsets.all(16.0), child: Center( - child: Text('No logs match your search in this category.'))) + child: + Text('No logs match your search in this category.'))) else ListView.builder( shrinkWrap: true, @@ -681,13 +671,11 @@ class _MarineManualReportStatusLogState ), ); - // --- 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), @@ -715,11 +703,10 @@ class _MarineManualReportStatusLogState _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 + _buildGranularStatus('API', log.apiStatusRaw), + _buildGranularStatus('FTP', log.ftpStatusRaw), _buildDetailRow('Message:', log.message), - // --- START: ADDED BUTTONS --- const Divider(height: 10), Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), @@ -727,19 +714,24 @@ class _MarineManualReportStatusLogState 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)), + 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)), + 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 --- ], ), ) @@ -755,8 +747,8 @@ class _MarineManualReportStatusLogState children: [ Expanded( flex: 2, - child: - Text(label, style: const TextStyle(fontWeight: FontWeight.bold))), + child: Text(label, + style: const TextStyle(fontWeight: FontWeight.bold))), const SizedBox(width: 8), Expanded(flex: 3, child: Text(value)), ], @@ -775,9 +767,9 @@ class _MarineManualReportStatusLogState return value.toString(); } - // --- START: ADDED WIDGET-BASED HEADER HELPER --- /// Builds a formatted category header row for the data list. - Widget _buildCategoryHeader(BuildContext context, String title, IconData icon) { + Widget _buildCategoryHeader( + BuildContext context, String title, IconData icon) { return Padding( padding: const EdgeInsets.only(top: 16.0, bottom: 8.0), child: Row( @@ -796,92 +788,79 @@ class _MarineManualReportStatusLogState ), ); } - // --- END: ADDED WIDGET-BASED HEADER HELPER --- - // --- START: RE-INTRODUCED TABLE-BASED HELPERS --- - /// Builds a formatted category header row for the data table. - TableRow _buildCategoryRow(BuildContext context, String title, IconData icon) { - return TableRow( + /// Builds a formatted Section header (replacement for _buildCategoryRow). + Widget _buildSectionHeader( + BuildContext context, String title, IconData icon) { + return Container( + margin: const EdgeInsets.only(top: 16.0, bottom: 8.0), + padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( - color: Colors.grey.shade100, + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(4), ), - children: [ - Padding( - padding: const EdgeInsets.only(top: 16.0, bottom: 8.0, left: 8.0, right: 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, - ), + child: Row( + children: [ + Icon(icon, size: 18, color: Theme.of(context).primaryColor), + const SizedBox(width: 8), + Expanded( + child: Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: Theme.of(context).primaryColor, ), - ], + ), ), - ), - const SizedBox.shrink(), // Empty cell for the second column - ], + ], + ), ); } - /// Builds a formatted row for the data dialog. - TableRow _buildDataTableRow(String label, String? value, {Color? valueColor}) { - String displayValue = (value == null || value.isEmpty || value == 'null') ? 'N/A' : value; + /// Builds a responsive data row (replacement for _buildDataTableRow). + /// Uses Column for better responsiveness on small screens if needed, + /// or Row with Expanded to allow wrapping. + Widget _buildResponsiveDataRow(String label, String? value, + {Color? valueColor}) { + String displayValue = + (value == null || value.isEmpty || value == 'null') ? 'N/A' : value; if (displayValue == '-999.0' || displayValue == '-999') { displayValue = 'N/A'; } - return TableRow( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - child: Text( - label, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.0, + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 4.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: Text( + label, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.0, + ), ), ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - child: Text( - displayValue, - style: TextStyle(fontSize: 14.0, color: valueColor), - ), - ), - ], - ); - } - - /// Builds a remark row for the data dialog. - TableRow _buildRemarkTableRow(String? remark) { - if (remark == null || remark.isEmpty) { - return const TableRow(children: [SizedBox.shrink(), SizedBox.shrink()]); - } - return TableRow( - children: [ - const SizedBox.shrink(), // Empty cell for the label - Padding( - padding: const EdgeInsets.only(bottom: 8.0, left: 8.0, right: 8.0), - child: Text( - "Remark: $remark", - style: TextStyle( - fontSize: 13.0, - color: Colors.grey.shade700, - fontStyle: FontStyle.italic, + const SizedBox(width: 8), + Expanded( + flex: 3, + child: Text( + displayValue, + style: TextStyle( + fontSize: 14.0, + color: (displayValue == 'N/A') ? Colors.grey : valueColor, + ), + textAlign: TextAlign.left, ), ), - ), - ], + ], + ), ); } - // --- END: RE-INTRODUCED TABLE-BASED HELPERS --- /// Shows the categorized and formatted data log in a dialog void _showDataDialog(BuildContext context, SubmissionLogEntry log) { @@ -889,178 +868,187 @@ class _MarineManualReportStatusLogState Widget dialogContent; // This will hold either a ListView or a Column if (log.type == 'Pre-Departure Checklist') { - // --- START: Handle Pre-Departure Checklist (uses Column/ListView) --- + // --- Handle Pre-Departure Checklist (Existing Logic) --- final items = Map.from(data['checklistItems'] ?? {}); final remarks = Map.from(data['remarks'] ?? {}); - // 1. Build the list of widgets final List contentWidgets = []; - // 2. Iterate over the DEFINED categories from the map for (final categoryEntry in _checklistSections.entries) { final categoryTitle = categoryEntry.key; final categoryItems = categoryEntry.value; - // Add the category header - contentWidgets.add(_buildCategoryHeader(context, categoryTitle, Icons.check_box_outlined)); + contentWidgets.add(_buildCategoryHeader( + context, categoryTitle, Icons.check_box_outlined)); - // 3. Add the items for that category - contentWidgets.addAll( - categoryItems.map((itemName) { - // Find the item's status and remark from the log data - final bool value = items[itemName] ?? false; // Default to 'No' - final String remark = remarks[itemName] ?? ''; - final String status = value ? 'Yes' : 'No'; + contentWidgets.addAll(categoryItems.map((itemName) { + final bool value = items[itemName] ?? false; + final String remark = remarks[itemName] ?? ''; + final String status = value ? 'Yes' : 'No'; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0), - child: Column( + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Row 1: Item and Status - Row( + Expanded( + flex: 3, + child: Text( + itemName, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.0, + ), + ), + ), + const SizedBox(width: 8), + 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, + ), + ), + ], + ), + if (remark.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 6.0, left: 8.0), + child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Item Name - Expanded( - flex: 3, - child: Text( - itemName, // Use the name from the category list - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.0, - ), + Text( + "Remark: ", + style: TextStyle( + fontSize: 13.0, + color: Colors.grey.shade700, + fontStyle: FontStyle.italic, ), ), - const SizedBox(width: 8), - // Status Expanded( - flex: 1, child: Text( - status, + remark, style: TextStyle( - fontSize: 14.0, - color: value ? Colors.green.shade700 : Colors.red.shade700, - fontWeight: FontWeight.bold, + fontSize: 13.0, + color: Colors.grey.shade700, + fontStyle: FontStyle.italic, ), - 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 + ), + ], + ), + ); + }).toList()); contentWidgets.add(const Divider(height: 16)); } - // 4. Handle any items that were in the log but NOT in the category map - final Set allCategorizedItems = _checklistSections.values.expand((list) => list).toSet(); + // Handle other items + final Set allCategorizedItems = + _checklistSections.values.expand((list) => list).toSet(); final List otherItems = []; for (final itemEntry in items.entries) { if (!allCategorizedItems.contains(itemEntry.key)) { - // This item was not in our hard-coded map final key = itemEntry.key; final value = itemEntry.value; final status = value ? 'Yes' : 'No'; final remark = remarks[key] ?? ''; - otherItems.add( - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0), - child: Column( + otherItems.add(Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(flex: 3, child: Text(key, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14.0))), - const SizedBox(width: 8), - 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)), - ], - ), - 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))), - ], - ), - ), + Expanded( + flex: 3, + child: Text(key, + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 14.0))), + const SizedBox(width: 8), + 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)), ], ), - ) - ); + 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))), + ], + ), + ), + ], + ), + )); } } if (otherItems.isNotEmpty) { - contentWidgets.add(_buildCategoryHeader(context, "Other Items", Icons.help_outline)); + contentWidgets.add( + _buildCategoryHeader(context, "Other Items", Icons.help_outline)); contentWidgets.addAll(otherItems); } 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 Table) --- - final List tableRows = []; + // --- START: Handle ALL OTHER Log Types (NOW USING RESPONSIVE WIDGETS) --- + final List listWidgets = []; // --- Helper for nested maps --- void addNestedMapRows(Map map) { map.forEach((key, value) { if (value is Map) { - // Handle nested maps (e.g., ysiSensorChecks) - tableRows.add(_buildDataTableRow(key, '')); + listWidgets.add(_buildResponsiveDataRow(key, '')); value.forEach((subKey, subValue) { - tableRows.add(_buildDataTableRow(' $subKey', subValue?.toString() ?? 'N/A')); + listWidgets.add(_buildResponsiveDataRow( + ' $subKey', subValue?.toString() ?? 'N/A')); }); } else { - tableRows.add(_buildDataTableRow(key, value?.toString() ?? 'N/A')); + listWidgets.add( + _buildResponsiveDataRow(key, value?.toString() ?? 'N/A')); } }); } @@ -1068,133 +1056,190 @@ class _MarineManualReportStatusLogState switch (log.type) { case 'Sonde Calibration': - tableRows.add(_buildCategoryRow(context, 'Sonde Info', Icons.info_outline)); - tableRows.add(_buildDataTableRow('Sonde Serial #', _getString(data, 'sondeSerialNumber'))); - tableRows.add(_buildDataTableRow('Firmware Version', _getString(data, 'firmwareVersion'))); - tableRows.add(_buildDataTableRow('KOR Version', _getString(data, 'korVersion'))); - tableRows.add(_buildDataTableRow('Location', _getString(data, 'location'))); - tableRows.add(_buildDataTableRow('Start Time', _getString(data, 'startDateTime'))); - tableRows.add(_buildDataTableRow('End Time', _getString(data, 'endDateTime'))); - tableRows.add(_buildDataTableRow('Status', _getString(data, 'calibration_status'))); - tableRows.add(_buildDataTableRow('Remarks', _getString(data, 'remarks'))); + listWidgets.add( + _buildSectionHeader(context, 'Sonde Info', Icons.info_outline)); + listWidgets.add(_buildResponsiveDataRow( + 'Sonde Serial #', _getString(data, 'sondeSerialNumber'))); + listWidgets.add(_buildResponsiveDataRow( + 'Firmware Version', _getString(data, 'firmwareVersion'))); + listWidgets.add(_buildResponsiveDataRow( + 'KOR Version', _getString(data, 'korVersion'))); + listWidgets.add( + _buildResponsiveDataRow('Location', _getString(data, 'location'))); + listWidgets.add(_buildResponsiveDataRow( + 'Start Time', _getString(data, 'startDateTime'))); + listWidgets.add(_buildResponsiveDataRow( + 'End Time', _getString(data, 'endDateTime'))); + listWidgets.add(_buildResponsiveDataRow( + 'Status', _getString(data, 'calibration_status'))); + listWidgets.add( + _buildResponsiveDataRow('Remarks', _getString(data, 'remarks'))); - tableRows.add(_buildCategoryRow(context, 'pH 7.0', Icons.science_outlined)); - tableRows.add(_buildDataTableRow('MV', _getString(data, 'ph_7_mv'))); - tableRows.add(_buildDataTableRow('Before', _getString(data, 'ph_7_before'))); - tableRows.add(_buildDataTableRow('After', _getString(data, 'ph_7_after'))); + listWidgets.add( + _buildSectionHeader(context, 'pH 7.0', Icons.science_outlined)); + listWidgets + .add(_buildResponsiveDataRow('MV', _getString(data, 'ph_7_mv'))); + listWidgets.add( + _buildResponsiveDataRow('Before', _getString(data, 'ph_7_before'))); + listWidgets.add( + _buildResponsiveDataRow('After', _getString(data, 'ph_7_after'))); - tableRows.add(_buildCategoryRow(context, 'pH 10.0', Icons.science_outlined)); - tableRows.add(_buildDataTableRow('MV', _getString(data, 'ph_10_mv'))); - tableRows.add(_buildDataTableRow('Before', _getString(data, 'ph_10_before'))); - tableRows.add(_buildDataTableRow('After', _getString(data, 'ph_10_after'))); + listWidgets.add( + _buildSectionHeader(context, 'pH 10.0', Icons.science_outlined)); + listWidgets.add( + _buildResponsiveDataRow('MV', _getString(data, 'ph_10_mv'))); + listWidgets.add(_buildResponsiveDataRow( + 'Before', _getString(data, 'ph_10_before'))); + listWidgets.add( + _buildResponsiveDataRow('After', _getString(data, 'ph_10_after'))); - tableRows.add(_buildCategoryRow(context, 'Conductivity', Icons.thermostat)); - tableRows.add(_buildDataTableRow('Before', _getString(data, 'cond_before'))); - tableRows.add(_buildDataTableRow('After', _getString(data, 'cond_after'))); + listWidgets.add( + _buildSectionHeader(context, 'Conductivity', Icons.thermostat)); + listWidgets.add(_buildResponsiveDataRow( + 'Before', _getString(data, 'cond_before'))); + listWidgets.add( + _buildResponsiveDataRow('After', _getString(data, 'cond_after'))); - tableRows.add(_buildCategoryRow(context, 'Dissolved Oxygen', Icons.air)); - tableRows.add(_buildDataTableRow('Before', _getString(data, 'do_before'))); - tableRows.add(_buildDataTableRow('After', _getString(data, 'do_after'))); + listWidgets.add( + _buildSectionHeader(context, 'Dissolved Oxygen', Icons.air)); + listWidgets.add( + _buildResponsiveDataRow('Before', _getString(data, 'do_before'))); + listWidgets.add( + _buildResponsiveDataRow('After', _getString(data, 'do_after'))); - tableRows.add(_buildCategoryRow(context, 'Turbidity', Icons.waves)); - tableRows.add(_buildDataTableRow('0 NTU Before', _getString(data, 'turbidity_0_before'))); - tableRows.add(_buildDataTableRow('0 NTU After', _getString(data, 'turbidity_0_after'))); - tableRows.add(_buildDataTableRow('124 NTU Before', _getString(data, 'turbidity_124_before'))); - tableRows.add(_buildDataTableRow('124 NTU After', _getString(data, 'turbidity_124_after'))); + listWidgets.add( + _buildSectionHeader(context, 'Turbidity', Icons.waves)); + listWidgets.add(_buildResponsiveDataRow( + '0 NTU Before', _getString(data, 'turbidity_0_before'))); + listWidgets.add(_buildResponsiveDataRow( + '0 NTU After', _getString(data, 'turbidity_0_after'))); + listWidgets.add(_buildResponsiveDataRow( + '124 NTU Before', _getString(data, 'turbidity_124_before'))); + listWidgets.add(_buildResponsiveDataRow( + '124 NTU After', _getString(data, 'turbidity_124_after'))); break; case 'Equipment Maintenance': - tableRows.add(_buildCategoryRow(context, 'YSI Sonde Checks', Icons.build_circle_outlined)); + listWidgets.add(_buildSectionHeader( + context, 'YSI Sonde Checks', Icons.build_circle_outlined)); if (data['ysiSondeChecks'] != null) { addNestedMapRows(Map.from(data['ysiSondeChecks'])); } - tableRows.add(_buildDataTableRow('Comments', _getString(data, 'ysiSondeComments'))); + listWidgets.add(_buildResponsiveDataRow( + 'Comments', _getString(data, 'ysiSondeComments'))); - tableRows.add(_buildCategoryRow(context, 'YSI Sensor Checks', Icons.sensors)); + listWidgets.add(_buildSectionHeader( + context, 'YSI Sensor Checks', Icons.sensors)); if (data['ysiSensorChecks'] != null) { - addNestedMapRows(Map.from(data['ysiSensorChecks'])); + addNestedMapRows( + Map.from(data['ysiSensorChecks'])); } - tableRows.add(_buildDataTableRow('Comments', _getString(data, 'ysiSensorComments'))); + listWidgets.add(_buildResponsiveDataRow( + 'Comments', _getString(data, 'ysiSensorComments'))); - tableRows.add(_buildCategoryRow(context, 'YSI Replacements', Icons.published_with_changes)); + listWidgets.add(_buildSectionHeader( + context, 'YSI Replacements', Icons.published_with_changes)); if (data['ysiReplacements'] != null) { - addNestedMapRows(Map.from(data['ysiReplacements'])); + addNestedMapRows( + Map.from(data['ysiReplacements'])); } - tableRows.add(_buildCategoryRow(context, 'Van Dorn Checks', Icons.opacity)); + listWidgets.add( + _buildSectionHeader(context, 'Van Dorn Checks', Icons.opacity)); if (data['vanDornChecks'] != null) { addNestedMapRows(Map.from(data['vanDornChecks'])); } - tableRows.add(_buildDataTableRow('Comments', _getString(data, 'vanDornComments'))); - tableRows.add(_buildDataTableRow('Current Serial', _getString(data, 'vanDornCurrentSerial'))); - tableRows.add(_buildDataTableRow('New Serial', _getString(data, 'vanDornNewSerial'))); + listWidgets.add(_buildResponsiveDataRow( + 'Comments', _getString(data, 'vanDornComments'))); + listWidgets.add(_buildResponsiveDataRow( + 'Current Serial', _getString(data, 'vanDornCurrentSerial'))); + listWidgets.add(_buildResponsiveDataRow( + 'New Serial', _getString(data, 'vanDornNewSerial'))); - tableRows.add(_buildCategoryRow(context, 'Van Dorn Replacements', Icons.published_with_changes)); + listWidgets.add(_buildSectionHeader(context, 'Van Dorn Replacements', + Icons.published_with_changes)); if (data['vanDornReplacements'] != null) { - addNestedMapRows(Map.from(data['vanDornReplacements'])); + addNestedMapRows( + Map.from(data['vanDornReplacements'])); } break; case 'NPE Report': - tableRows.add(_buildCategoryRow(context, 'Event Info', Icons.calendar_today)); - tableRows.add(_buildDataTableRow('Date', _getString(data, 'eventDate'))); - tableRows.add(_buildDataTableRow('Time', _getString(data, 'eventTime'))); - tableRows.add(_buildDataTableRow('Sampler', _getString(data, 'firstSamplerName'))); + listWidgets.add( + _buildSectionHeader(context, 'Event Info', Icons.calendar_today)); + listWidgets.add( + _buildResponsiveDataRow('Date', _getString(data, 'eventDate'))); + listWidgets.add( + _buildResponsiveDataRow('Time', _getString(data, 'eventTime'))); + listWidgets.add(_buildResponsiveDataRow( + 'Sampler', _getString(data, 'firstSamplerName'))); - tableRows.add(_buildCategoryRow(context, 'Location', Icons.location_on_outlined)); + listWidgets.add(_buildSectionHeader( + context, 'Location', Icons.location_on_outlined)); if (data['selectedStation'] != null) { - tableRows.add(_buildDataTableRow('Station', _getString(data['selectedStation'], 'man_station_name') ?? _getString(data['selectedStation'], 'tbl_station_name'))); + listWidgets.add(_buildResponsiveDataRow( + 'Station', + _getString(data['selectedStation'], 'man_station_name') ?? + _getString(data['selectedStation'], 'tbl_station_name'))); } else { - tableRows.add(_buildDataTableRow('Location', _getString(data, 'locationDescription'))); - tableRows.add(_buildDataTableRow('State', _getString(data, 'stateName'))); + listWidgets.add(_buildResponsiveDataRow( + 'Location', _getString(data, 'locationDescription'))); + listWidgets.add( + _buildResponsiveDataRow('State', _getString(data, 'stateName'))); } - tableRows.add(_buildDataTableRow('Latitude', _getString(data, 'latitude'))); - tableRows.add(_buildDataTableRow('Longitude', _getString(data, 'longitude'))); + listWidgets.add( + _buildResponsiveDataRow('Latitude', _getString(data, 'latitude'))); + listWidgets.add(_buildResponsiveDataRow( + 'Longitude', _getString(data, 'longitude'))); - tableRows.add(_buildCategoryRow(context, 'Parameters', Icons.bar_chart)); - tableRows.add(_buildDataTableRow('Oxygen Conc (mg/L)', _getString(data, 'oxygenConcentration'))); - tableRows.add(_buildDataTableRow('Oxygen Sat (%)', _getString(data, 'oxygenSaturation'))); - tableRows.add(_buildDataTableRow('pH', _getString(data, 'ph'))); - tableRows.add(_buildDataTableRow('Conductivity (ÂĩS/cm)', _getString(data, 'electricalConductivity'))); - tableRows.add(_buildDataTableRow('Temperature (°C)', _getString(data, 'temperature'))); - tableRows.add(_buildDataTableRow('Turbidity (NTU)', _getString(data, 'turbidity'))); + listWidgets.add( + _buildSectionHeader(context, 'Parameters', Icons.bar_chart)); + listWidgets.add(_buildResponsiveDataRow( + 'Oxygen Conc (mg/L)', _getString(data, 'oxygenConcentration'))); + listWidgets.add(_buildResponsiveDataRow( + 'Oxygen Sat (%)', _getString(data, 'oxygenSaturation'))); + listWidgets.add(_buildResponsiveDataRow('pH', _getString(data, 'ph'))); + listWidgets.add(_buildResponsiveDataRow('Conductivity (ÂĩS/cm)', + _getString(data, 'electricalConductivity'))); + listWidgets.add(_buildResponsiveDataRow( + 'Temperature (°C)', _getString(data, 'temperature'))); + listWidgets.add(_buildResponsiveDataRow( + 'Turbidity (NTU)', _getString(data, 'turbidity'))); - tableRows.add(_buildCategoryRow(context, 'Observations', Icons.warning_amber_rounded)); + listWidgets.add(_buildSectionHeader( + context, 'Observations', Icons.warning_amber_rounded)); if (data['fieldObservations'] != null) { - final observations = Map.from(data['fieldObservations']); + final observations = + Map.from(data['fieldObservations']); observations.forEach((key, value) { - if(value) tableRows.add(_buildDataTableRow(key, 'Checked')); + if (value) listWidgets.add(_buildResponsiveDataRow(key, 'Checked')); }); } - tableRows.add(_buildDataTableRow('Other Remarks', _getString(data, 'othersObservationRemark'))); - tableRows.add(_buildDataTableRow('Possible Source', _getString(data, 'possibleSource'))); + listWidgets.add(_buildResponsiveDataRow( + 'Other Remarks', _getString(data, 'othersObservationRemark'))); + listWidgets.add(_buildResponsiveDataRow( + 'Possible Source', _getString(data, 'possibleSource'))); if (data['selectedTarballClassification'] != null) { - tableRows.add(_buildDataTableRow('Tarball Class', _getString(data['selectedTarballClassification'], 'classification_name'))); + listWidgets.add(_buildResponsiveDataRow( + 'Tarball Class', + _getString(data['selectedTarballClassification'], + 'classification_name'))); } break; default: - tableRows.add(_buildDataTableRow('Error', 'No data view configured for log type: ${log.type}')); + listWidgets.add(_buildResponsiveDataRow( + 'Error', 'No data view configured for log type: ${log.type}')); } - // Assign the Table as the content for the 'else' block - dialogContent = Table( - columnWidths: const { - 0: IntrinsicColumnWidth(), // Label column - 1: FlexColumnWidth(), // Value column - }, - border: TableBorder( - horizontalInside: BorderSide( - color: Colors.grey.shade300, - width: 0.5, - ), - ), - children: tableRows, + // Assign the content + dialogContent = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: listWidgets, ); // --- END: Handle ALL OTHER Log Types --- } - // Now, dialogContent is guaranteed to be assigned showDialog( context: context, builder: (context) { @@ -1203,7 +1248,7 @@ class _MarineManualReportStatusLogState content: SizedBox( width: double.maxFinite, child: SingleChildScrollView( - child: dialogContent, // <-- This is now safe + child: dialogContent, ), ), actions: [ @@ -1218,7 +1263,6 @@ class _MarineManualReportStatusLogState } // --- END: MODIFIED METHOD --- - /// Shows the image gallery dialog void _showImageDialog(BuildContext context, SubmissionLogEntry log) { final List imageEntries = []; @@ -1261,7 +1305,8 @@ class _MarineManualReportStatusLogState ), itemBuilder: (context, index) { final imageEntry = imageEntries[index]; - final bool hasRemark = imageEntry.remark != null && imageEntry.remark!.isNotEmpty; + final bool hasRemark = + imageEntry.remark != null && imageEntry.remark!.isNotEmpty; return Card( clipBehavior: Clip.antiAlias, @@ -1329,7 +1374,8 @@ class _MarineManualReportStatusLogState } /// Helper for _showImageDialog - void _addImagesToList(SubmissionLogEntry log, Map imageRemarkMap, List imageEntries) { + void _addImagesToList(SubmissionLogEntry log, + Map imageRemarkMap, List imageEntries) { for (final entry in imageRemarkMap.entries) { final imageKey = entry.key; final remarkKey = entry.value; @@ -1338,7 +1384,8 @@ class _MarineManualReportStatusLogState 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); + final remark = + (remarkKey != null ? log.rawData[remarkKey] as String? : null); imageEntries.add(ImageLogEntry(file: file, remark: remark)); } } @@ -1374,17 +1421,21 @@ class _MarineManualReportStatusLogState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('$type Status:', style: const TextStyle(fontWeight: FontWeight.bold)), + Text('$type Status:', + style: const TextStyle(fontWeight: FontWeight.bold)), ...statuses.map((s) { - final serverName = s['server_name'] ?? s['config_name'] ?? 'Server N/A'; + 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 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), + padding: + const EdgeInsets.symmetric(vertical: 3.0, horizontal: 8.0), child: Row( children: [ Icon(icon, size: 16, color: color), diff --git a/lib/screens/marine/manual/reports/npe_report_from_in_situ.dart b/lib/screens/marine/manual/reports/npe_report_from_in_situ.dart index dec9fbc..dd44e29 100644 --- a/lib/screens/marine/manual/reports/npe_report_from_in_situ.dart +++ b/lib/screens/marine/manual/reports/npe_report_from_in_situ.dart @@ -166,6 +166,16 @@ class _NPEReportFromInSituState extends State { _locationController.clear(); _latController.clear(); _longController.clear(); + + // --- CHANGE: Clear measurement controllers so they are empty before reading --- + _doPercentController.clear(); + _doMgLController.clear(); + _phController.clear(); + _condController.clear(); + _turbController.clear(); + _tempController.clear(); + // --------------------------------------------------------------------------- + _setDefaultDateTime(); // Reset to 'now' }); } @@ -252,7 +262,7 @@ class _NPEReportFromInSituState extends State { final auth = Provider.of(context, listen: false); final service = Provider.of(context, listen: false); - _npeData.firstSamplerName = auth.profileData?['user_name']; + _npeData.firstSamplerName = auth.profileData?['first_name']; _npeData.firstSamplerUserId = auth.profileData?['user_id']; _npeData.eventDate = _eventDateTimeController.text.split(' ')[0]; _npeData.eventTime = _eventDateTimeController.text.split(' ').length > 1 ? _eventDateTimeController.text.split(' ')[1] : ''; @@ -1007,6 +1017,10 @@ class _NPEReportFromInSituState extends State { border: InputBorder.none, contentPadding: EdgeInsets.zero, isDense: true, // Helps with alignment + // --- CHANGE: Added hint text to display when controller is empty (before start reading) --- + hintText: '-.--', + hintStyle: TextStyle(color: Colors.grey), + // ----------------------------------------------------------------------------------------- ), ), ), diff --git a/lib/screens/marine/manual/reports/npe_report_from_tarball.dart b/lib/screens/marine/manual/reports/npe_report_from_tarball.dart index e4fc6b0..bc380ad 100644 --- a/lib/screens/marine/manual/reports/npe_report_from_tarball.dart +++ b/lib/screens/marine/manual/reports/npe_report_from_tarball.dart @@ -182,7 +182,7 @@ class _NPEReportFromTarballState extends State { final auth = Provider.of(context, listen: false); final service = Provider.of(context, listen: false); - _npeData.firstSamplerName = auth.profileData?['user_name']; + _npeData.firstSamplerName = auth.profileData?['first_name']; _npeData.firstSamplerUserId = auth.profileData?['user_id']; _npeData.eventDate = _eventDateTimeController.text.split(' ')[0]; _npeData.eventTime = _eventDateTimeController.text.split(' ').length > 1 ? _eventDateTimeController.text.split(' ')[1] : ''; diff --git a/lib/screens/marine/manual/reports/npe_report_new_location.dart b/lib/screens/marine/manual/reports/npe_report_new_location.dart index 1360770..d7fcb42 100644 --- a/lib/screens/marine/manual/reports/npe_report_new_location.dart +++ b/lib/screens/marine/manual/reports/npe_report_new_location.dart @@ -172,7 +172,7 @@ class _NPEReportNewLocationState extends State { final auth = Provider.of(context, listen: false); final service = Provider.of(context, listen: false); - _npeData.firstSamplerName = auth.profileData?['user_name']; + _npeData.firstSamplerName = auth.profileData?['first_name']; _npeData.firstSamplerUserId = auth.profileData?['user_id']; _npeData.eventDate = _eventDateTimeController.text.split(' ')[0]; _npeData.eventTime = _eventDateTimeController.text.split(' ').length > 1 ? _eventDateTimeController.text.split(' ')[1] : ''; diff --git a/lib/screens/marine/manual/widgets/in_situ_step_4_summary.dart b/lib/screens/marine/manual/widgets/in_situ_step_4_summary.dart index 5afff19..968d3dd 100644 --- a/lib/screens/marine/manual/widgets/in_situ_step_4_summary.dart +++ b/lib/screens/marine/manual/widgets/in_situ_step_4_summary.dart @@ -1,7 +1,7 @@ // lib/screens/marine/manual/widgets/in_situ_step_4_summary.dart import 'dart:io'; -import 'dart:typed_data'; // <-- Required for Uint8List +import 'dart:typed_data'; // Required for Uint8List import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -12,8 +12,7 @@ import '../reports/npe_report_from_in_situ.dart'; class InSituStep4Summary extends StatefulWidget { final InSituSamplingData data; - final Future> Function() - onSubmit; // Expects a function that returns the submission result + final Future> Function() onSubmit; // Expects a function that returns the submission result final bool isLoading; const InSituStep4Summary({ @@ -43,11 +42,20 @@ class _InSituStep4SummaryState extends State { 'batteryVoltage': 'Battery', }; + // --- START: FIXED OUT OF BOUNDS LOGIC --- Set _getOutOfBoundsKeys(BuildContext context) { final authProvider = Provider.of(context, listen: false); final marineLimits = authProvider.marineParameterLimits ?? []; final Set invalidKeys = {}; - final int? stationId = widget.data.selectedStation?['man_station_id']; + + // ✅ FIX: Robustly try to get station ID using both potential keys + final dynamic stationId = widget.data.selectedStation?['station_id'] ?? + widget.data.selectedStation?['man_station_id']; + + if (stationId == null) { + // Without a station ID, we cannot check station-specific limits + return invalidKeys; + } final readings = { 'oxygenConcentration': widget.data.oxygenConcentration, @@ -75,20 +83,13 @@ class _InSituStep4SummaryState extends State { final limitName = _parameterKeyToLimitName[key]; if (limitName == null) return; - Map limitData = {}; - - if (stationId != null) { - // --- START FIX: Use type-safe comparison for station_id --- - // This ensures that the comparison works regardless of whether - // station_id is stored as a number (e.g., 123) or a string (e.g., "123"). - limitData = marineLimits.firstWhere( - (l) => - l['param_parameter_list'] == limitName && - l['station_id']?.toString() == stationId.toString(), - orElse: () => {}, - ); - // --- END FIX --- - } + // Find limits for this specific station + final limitData = marineLimits.firstWhere( + (l) => + l['param_parameter_list'] == limitName && + l['station_id']?.toString() == stationId.toString(), + orElse: () => {}, + ); if (limitData.isNotEmpty) { final lowerLimit = parseLimitValue(limitData['param_lower_limit']); @@ -103,6 +104,7 @@ class _InSituStep4SummaryState extends State { return invalidKeys; } + // --- END: FIXED OUT OF BOUNDS LOGIC --- /// Checks captured data against NPE limits and returns detailed information for the dialog. List> _getNpeTriggeredParameters(BuildContext context) { @@ -136,7 +138,6 @@ class _InSituStep4SummaryState extends State { final limitName = _parameterKeyToLimitName[key]; if (limitName == null) return; - // NPE limits are general and NOT station-specific, so this is correct. final limitData = npeLimits.firstWhere( (l) => l['param_parameter_list'] == limitName, orElse: () => {}, @@ -307,7 +308,6 @@ class _InSituStep4SummaryState extends State { ); } - /// Handles the complete submission flow: NPE check, submission, and UI feedback/navigation. Future _handleSubmit(BuildContext context) async { if (_isHandlingSubmit || widget.isLoading) return; @@ -327,14 +327,12 @@ class _InSituStep4SummaryState extends State { } else if (userChoice == false) { proceedWithSubmission = true; } - // If userChoice is null (dialog dismissed), we do nothing. } else { - // No NPE hit, proceed normally. proceedWithSubmission = true; } if (proceedWithSubmission) { - final result = await widget.onSubmit(); // This calls the logic in the parent + final result = await widget.onSubmit(); if (!mounted) return; final message = result['message'] ?? 'An unknown error occurred.'; @@ -348,14 +346,13 @@ class _InSituStep4SummaryState extends State { if (result['success'] == true) { if (shouldOpenNpeReport) { - // Navigate to the correct screen without passing data, as requested. Navigator.pushAndRemoveUntil( context, - MaterialPageRoute(builder: (context) => const NPEReportFromInSitu()), + MaterialPageRoute( + builder: (context) => const NPEReportFromInSitu()), (route) => route.isFirst, ); } else { - // Submission successful, and no NPE report needed, so go home. Navigator.of(context).popUntil((route) => route.isFirst); } } @@ -369,6 +366,7 @@ class _InSituStep4SummaryState extends State { @override Widget build(BuildContext context) { + // Get the invalid keys based on station limits final outOfBoundsKeys = _getOutOfBoundsKeys(context); return ListView( @@ -463,6 +461,7 @@ class _InSituStep4SummaryState extends State { _buildDetailRow("Capture Time:", "${widget.data.dataCaptureDate} ${widget.data.dataCaptureTime}"), const Divider(height: 20), + // Pass 'isOutOfBounds' based on the calculated keys _buildParameterListItem(context, icon: Icons.air, label: "Oxygen Conc.", @@ -635,7 +634,6 @@ class _InSituStep4SummaryState extends State { const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), const SizedBox(height: 8), if (image != null) - // --- START MODIFICATION: Use FutureBuilder to load bytes async --- ClipRRect( borderRadius: BorderRadius.circular(8.0), child: FutureBuilder( @@ -667,7 +665,6 @@ class _InSituStep4SummaryState extends State { }, ), ) - // --- END MODIFICATION --- else Container( height: 100, diff --git a/lib/screens/settings/telegram_alert_settings.dart b/lib/screens/settings/telegram_alert_settings.dart index e47d565..3516944 100644 --- a/lib/screens/settings/telegram_alert_settings.dart +++ b/lib/screens/settings/telegram_alert_settings.dart @@ -6,10 +6,7 @@ import 'package:environment_monitoring_app/auth_provider.dart'; import 'package:environment_monitoring_app/services/settings_service.dart'; class TelegramAlertSettingsScreen extends StatelessWidget { - // --- START MODIFICATION --- - // Removed 'const' from the constructor to fix the error. TelegramAlertSettingsScreen({super.key}); - // --- END MODIFICATION --- // Helper service for parsing settings final SettingsService _settingsService = SettingsService(); @@ -56,6 +53,7 @@ class TelegramAlertSettingsScreen extends StatelessWidget { padding: const EdgeInsets.all(16.0), child: Column( children: [ + // --- MARINE SECTION --- ExpansionTile( title: const Text('Marine Alerts', style: TextStyle(fontWeight: FontWeight.bold)), @@ -69,8 +67,14 @@ class TelegramAlertSettingsScreen extends StatelessWidget { 'Investigative', _settingsService .getMarineInvestigativeChatId(appSettings)), + // --- ADDED: Marine Report --- + _buildChatIdEntry( + 'NPE Report', + _settingsService.getMarineReportChatId(appSettings)), ], ), + + // --- RIVER SECTION --- ExpansionTile( title: const Text('River Alerts', style: TextStyle(fontWeight: FontWeight.bold)), @@ -86,8 +90,14 @@ class TelegramAlertSettingsScreen extends StatelessWidget { 'Investigative', _settingsService .getRiverInvestigativeChatId(appSettings)), + // --- ADDED: River Report --- + _buildChatIdEntry( + 'NPE Report', + _settingsService.getRiverReportChatId(appSettings)), ], ), + + // --- AIR SECTION --- ExpansionTile( title: const Text('Air Alerts', style: TextStyle(fontWeight: FontWeight.bold)), diff --git a/lib/services/marine_in_situ_sampling_service.dart b/lib/services/marine_in_situ_sampling_service.dart index 1414856..14b0635 100644 --- a/lib/services/marine_in_situ_sampling_service.dart +++ b/lib/services/marine_in_situ_sampling_service.dart @@ -663,7 +663,7 @@ class MarineInSituSamplingService { final submitter = data.firstSamplerName ?? 'N/A'; final buffer = StringBuffer() - ..writeln('✅ *In-Situ Sample $submissionType Submitted:*') + ..writeln('✅ *Marine In-Situ Sample $submissionType Submitted:*') ..writeln() ..writeln('*Station Name & Code:* $stationName ($stationCode)') // --- START MODIFICATION --- @@ -736,8 +736,9 @@ class MarineInSituSamplingService { final allLimits = await _dbHelper.loadMarineParameterLimits() ?? []; if (allLimits.isEmpty) return ""; - // --- START FIX: Use correct key 'man_station_id' --- - final dynamic stationId = data.selectedStation?['man_station_id']; + // --- START FIX: Use correct key 'station_id' with fallback to 'man_station_id' --- + // The original code was strictly checking 'man_station_id', but API form data uses 'station_id'. + final dynamic stationId = data.selectedStation?['station_id'] ?? data.selectedStation?['man_station_id']; // --- END FIX --- if (stationId == null) return ""; // Cannot check limits without a station ID @@ -880,7 +881,7 @@ class MarineInSituSamplingService { final buffer = StringBuffer() ..writeln() ..writeln(' ') - ..writeln('🚨 *NPE Parameter Limit Detected:*') + ..writeln('🚨 *Marine NPE Parameter Limit Detected:*') ..writeln('The following parameters triggered an NPE alert:'); buffer.writeAll(npeMessages, '\n'); diff --git a/lib/services/marine_npe_report_service.dart b/lib/services/marine_npe_report_service.dart index 077428e..207c608 100644 --- a/lib/services/marine_npe_report_service.dart +++ b/lib/services/marine_npe_report_service.dart @@ -1,4 +1,4 @@ -// lib/services/marine_npe_report_service.dart +// lib/services/marine_tarball_sampling_service.dart import 'dart:async'; import 'dart:io'; diff --git a/lib/services/retry_service.dart b/lib/services/retry_service.dart index 585acb4..263d17e 100644 --- a/lib/services/retry_service.dart +++ b/lib/services/retry_service.dart @@ -9,17 +9,28 @@ import 'package:environment_monitoring_app/models/in_situ_sampling_data.dart'; import 'package:environment_monitoring_app/services/marine_in_situ_sampling_service.dart'; import 'package:environment_monitoring_app/models/river_in_situ_sampling_data.dart'; import 'package:environment_monitoring_app/services/river_in_situ_sampling_service.dart'; -// *** ADDED: Import River Investigative Model and Service *** import 'package:environment_monitoring_app/models/river_inves_manual_sampling_data.dart'; import 'package:environment_monitoring_app/services/river_investigative_sampling_service.dart'; -// *** END ADDED *** import 'package:environment_monitoring_app/models/marine_inves_manual_sampling_data.dart'; import 'package:environment_monitoring_app/services/marine_investigative_sampling_service.dart'; import 'package:environment_monitoring_app/models/tarball_data.dart'; import 'package:environment_monitoring_app/services/marine_tarball_sampling_service.dart'; -//import 'package:environment_monitoring_app/services/api_service.dart'; -import 'package:environment_monitoring_app/services/database_helper.dart'; +// --- MARINE REPORT IMPORTS --- +import 'package:environment_monitoring_app/models/marine_manual_npe_report_data.dart'; +import 'package:environment_monitoring_app/services/marine_npe_report_service.dart'; + +import 'package:environment_monitoring_app/models/marine_manual_pre_departure_checklist_data.dart'; +import 'package:environment_monitoring_app/services/marine_manual_pre_departure_service.dart'; + +import 'package:environment_monitoring_app/models/marine_manual_sonde_calibration_data.dart'; +import 'package:environment_monitoring_app/services/marine_manual_sonde_calibration_service.dart'; + +import 'package:environment_monitoring_app/models/marine_manual_equipment_maintenance_data.dart'; +import 'package:environment_monitoring_app/services/marine_manual_equipment_maintenance_service.dart'; +// --- END MARINE REPORT IMPORTS --- + +import 'package:environment_monitoring_app/services/database_helper.dart'; import 'package:environment_monitoring_app/services/base_api_service.dart'; import 'package:environment_monitoring_app/services/ftp_service.dart'; import 'package:environment_monitoring_app/services/server_config_service.dart'; @@ -33,33 +44,51 @@ class RetryService { final ServerConfigService _serverConfigService = ServerConfigService(); bool _isProcessing = false; + // Sampling Services MarineInSituSamplingService? _marineInSituService; RiverInSituSamplingService? _riverInSituService; MarineInvestigativeSamplingService? _marineInvestigativeService; - MarineTarballSamplingService? _marineTarballService; - // *** ADDED: River Investigative Service member *** RiverInvestigativeSamplingService? _riverInvestigativeService; + MarineTarballSamplingService? _marineTarballService; + + // Report Services + MarineNpeReportService? _marineNpeService; + // *** ADDED: Other Marine Report Services *** + MarineManualPreDepartureService? _marinePreDepartureService; + MarineManualSondeCalibrationService? _marineSondeCalibrationService; + MarineManualEquipmentMaintenanceService? _marineEquipmentMaintenanceService; // *** END ADDED *** + AuthProvider? _authProvider; - // *** MODIFIED: Added riverInvestigativeService to initialize *** void initialize({ required MarineInSituSamplingService marineInSituService, required RiverInSituSamplingService riverInSituService, required MarineInvestigativeSamplingService marineInvestigativeService, - required RiverInvestigativeSamplingService riverInvestigativeService, // <-- Added parameter + required RiverInvestigativeSamplingService riverInvestigativeService, required MarineTarballSamplingService marineTarballService, + + // Added Marine Report Services + required MarineNpeReportService marineNpeService, + required MarineManualPreDepartureService marinePreDepartureService, + required MarineManualSondeCalibrationService marineSondeCalibrationService, + required MarineManualEquipmentMaintenanceService marineEquipmentMaintenanceService, + required AuthProvider authProvider, }) { _marineInSituService = marineInSituService; _riverInSituService = riverInSituService; _marineInvestigativeService = marineInvestigativeService; - _riverInvestigativeService = riverInvestigativeService; // <-- Assign parameter + _riverInvestigativeService = riverInvestigativeService; _marineTarballService = marineTarballService; + + _marineNpeService = marineNpeService; + _marinePreDepartureService = marinePreDepartureService; + _marineSondeCalibrationService = marineSondeCalibrationService; + _marineEquipmentMaintenanceService = marineEquipmentMaintenanceService; + _authProvider = authProvider; } - // *** END MODIFIED *** - /// Adds a generic, complex task to the queue, to be handled by a background processor. Future queueTask({ @@ -68,7 +97,7 @@ class RetryService { }) async { await _dbHelper.queueFailedRequest({ 'type': type, - 'endpoint_or_path': 'N/A', // Not applicable for complex tasks initially + 'endpoint_or_path': 'N/A', 'payload': jsonEncode(payload), 'timestamp': DateTime.now().toIso8601String(), 'status': 'pending', @@ -84,13 +113,12 @@ class RetryService { Map? fields, Map? files, }) async { - // Convert File objects to paths for JSON serialization final serializableFiles = files?.map((key, value) => MapEntry(key, value.path)); final payload = { 'method': method, 'body': body, 'fields': fields, - 'files': serializableFiles, // Store paths instead of File objects + 'files': serializableFiles, }; await _dbHelper.queueFailedRequest({ 'type': 'api', @@ -106,11 +134,11 @@ class RetryService { Future addFtpToQueue({ required String localFilePath, required String remotePath, - required int ftpConfigId, // Added to specify which destination failed + required int ftpConfigId, }) async { final payload = { 'localFilePath': localFilePath, - 'ftpConfigId': ftpConfigId, // Store the specific config ID + 'ftpConfigId': ftpConfigId, }; await _dbHelper.queueFailedRequest({ 'type': 'ftp', @@ -123,12 +151,10 @@ class RetryService { } - /// Retrieves all tasks currently in the 'pending' state from the queue. Future>> getPendingTasks() { return _dbHelper.getPendingRequests(); } - /// Processes the entire queue of pending tasks. Future processRetryQueue() async { if (_isProcessing) { debugPrint("[RetryService] âŗ Queue is already being processed. Skipping."); @@ -144,7 +170,6 @@ class RetryService { return; } - // Check internet connection *before* processing if (_authProvider == null || !await _authProvider!.isConnected()) { debugPrint("[RetryService] ❌ No internet connection. Aborting queue processing."); _isProcessing = false; @@ -152,44 +177,36 @@ class RetryService { } debugPrint("[RetryService] 🔎 Found ${pendingTasks.length} pending tasks."); - // Process tasks one by one for (final task in pendingTasks) { - // Add safety check in case a task is deleted mid-processing by another call if (await _dbHelper.getRequestById(task['id'] as int) != null) { await retryTask(task['id'] as int); } - // Optional: Add a small delay between tasks if needed - // await Future.delayed(Duration(seconds: 1)); } debugPrint("[RetryService] âšī¸ Finished processing retry queue."); _isProcessing = false; } - /// Attempts to re-execute a single failed task from the queue. - /// Returns `true` on success, `false` on failure. Future retryTask(int taskId) async { final task = await _dbHelper.getRequestById(taskId); if (task == null) { - debugPrint("Retry failed: Task with ID $taskId not found in the queue (might have been processed already)."); - return false; // Task doesn't exist or was processed elsewhere + debugPrint("Retry failed: Task with ID $taskId not found in the queue."); + return false; } bool success = false; - Map payload; // Declare outside try-catch - final String taskType = task['type'] as String; // Get type early for logging + Map payload; + final String taskType = task['type'] as String; try { - payload = jsonDecode(task['payload'] as String); // Decode payload inside try + payload = jsonDecode(task['payload'] as String); } catch (e) { debugPrint("Error decoding payload for task $taskId (Type: $taskType): $e. Removing invalid task."); await _dbHelper.deleteRequestFromQueue(taskId); - return false; // Cannot process without valid payload + return false; } - try { - // Ensure AuthProvider is initialized and we are online (checked in processRetryQueue) if (_authProvider == null) { debugPrint("RetryService has not been initialized. Cannot process task $taskId."); return false; @@ -197,121 +214,66 @@ class RetryService { // --- Complex Task Handlers --- if (taskType == 'insitu_submission') { - debugPrint("Retrying complex task 'insitu_submission' with ID $taskId."); - if (_marineInSituService == null) { - debugPrint("Retry failed: MarineInSituSamplingService not initialized."); - return false; - } - - final String logDirectoryPath = payload['localLogPath']; // Path to the directory + if (_marineInSituService == null) return false; + final String logDirectoryPath = payload['localLogPath']; final jsonFilePath = p.join(logDirectoryPath, 'data.json'); final file = File(jsonFilePath); - if (!await file.exists()) { - debugPrint("Retry failed: Source log file no longer exists at $jsonFilePath"); - await _dbHelper.deleteRequestFromQueue(taskId); // Remove invalid task + await _dbHelper.deleteRequestFromQueue(taskId); return false; } - final content = await file.readAsString(); - final jsonData = jsonDecode(content) as Map; - final InSituSamplingData dataToResubmit = InSituSamplingData.fromJson(jsonData); - - // Re-run the original submission logic, passing the log directory final result = await _marineInSituService!.submitInSituSample( - data: dataToResubmit, - appSettings: _authProvider!.appSettings, // Get current settings + data: InSituSamplingData.fromJson(jsonDecode(content)), + appSettings: _authProvider!.appSettings, authProvider: _authProvider!, - logDirectory: logDirectoryPath, // Pass directory to update log + logDirectory: logDirectoryPath, ); success = result['success']; } else if (taskType == 'river_insitu_submission') { - debugPrint("Retrying complex task 'river_insitu_submission' with ID $taskId."); - if (_riverInSituService == null) { - debugPrint("Retry failed: RiverInSituSamplingService not initialized."); - return false; - } - - final String jsonFilePath = payload['localLogPath']; // Path to the JSON file + if (_riverInSituService == null) return false; + final String jsonFilePath = payload['localLogPath']; final file = File(jsonFilePath); - if (!await file.exists()) { - debugPrint("Retry failed: Source log file no longer exists at $jsonFilePath"); await _dbHelper.deleteRequestFromQueue(taskId); return false; } - final String logDirectoryPath = p.dirname(jsonFilePath); // Get directory from file path - - - final content = await file.readAsString(); - final jsonData = jsonDecode(content) as Map; - final RiverInSituSamplingData dataToResubmit = RiverInSituSamplingData.fromJson(jsonData); - final result = await _riverInSituService!.submitData( - data: dataToResubmit, + data: RiverInSituSamplingData.fromJson(jsonDecode(await file.readAsString())), appSettings: _authProvider!.appSettings, authProvider: _authProvider!, - logDirectory: logDirectoryPath, + logDirectory: p.dirname(jsonFilePath), ); success = result['success']; - // *** ADDED: Handler for river_investigative_submission *** } else if (taskType == 'river_investigative_submission') { - debugPrint("Retrying complex task 'river_investigative_submission' with ID $taskId."); - if (_riverInvestigativeService == null) { - debugPrint("Retry failed: RiverInvestigativeSamplingService not initialized."); - return false; - } - - final String jsonFilePath = payload['localLogPath']; // Path to the JSON file + if (_riverInvestigativeService == null) return false; + final String jsonFilePath = payload['localLogPath']; final file = File(jsonFilePath); - if (!await file.exists()) { - debugPrint("Retry failed: Source log file no longer exists at $jsonFilePath"); await _dbHelper.deleteRequestFromQueue(taskId); return false; } - final String logDirectoryPath = p.dirname(jsonFilePath); // Get directory from file path - - final content = await file.readAsString(); - final jsonData = jsonDecode(content) as Map; - // Use the correct Investigative data model - final RiverInvesManualSamplingData dataToResubmit = RiverInvesManualSamplingData.fromJson(jsonData); - - // Call the submitData method from the Investigative service final result = await _riverInvestigativeService!.submitData( - data: dataToResubmit, + data: RiverInvesManualSamplingData.fromJson(jsonDecode(await file.readAsString())), appSettings: _authProvider!.appSettings, authProvider: _authProvider!, - logDirectory: logDirectoryPath, + logDirectory: p.dirname(jsonFilePath), ); success = result['success']; - // *** END ADDED *** } else if (taskType == 'investigative_submission') { - debugPrint("Retrying complex task 'investigative_submission' with ID $taskId."); - if (_marineInvestigativeService == null) { - debugPrint("Retry failed: MarineInvestigativeSamplingService not initialized."); - return false; - } - - final String logDirectoryPath = payload['localLogPath']; // Path to the directory + if (_marineInvestigativeService == null) return false; + final String logDirectoryPath = payload['localLogPath']; final jsonFilePath = p.join(logDirectoryPath, 'data.json'); final file = File(jsonFilePath); - if (!await file.exists()) { - debugPrint("Retry failed: Source log file no longer exists at $jsonFilePath"); await _dbHelper.deleteRequestFromQueue(taskId); return false; } - - final content = await file.readAsString(); - final jsonData = jsonDecode(content) as Map; - final MarineInvesManualSamplingData dataToResubmit = MarineInvesManualSamplingData.fromJson(jsonData); - final result = await _marineInvestigativeService!.submitInvestigativeSample( - data: dataToResubmit, + data: MarineInvesManualSamplingData.fromJson(jsonDecode(await file.readAsString())), appSettings: _authProvider!.appSettings, authProvider: _authProvider!, logDirectory: logDirectoryPath, @@ -319,191 +281,377 @@ class RetryService { success = result['success']; } else if (taskType == 'tarball_submission') { - debugPrint("Retrying complex task 'tarball_submission' with ID $taskId."); - if (_marineTarballService == null) { - debugPrint("Retry failed: MarineTarballSamplingService not initialized."); - return false; - } - - final String logDirectoryPath = payload['localLogPath']; // Path to the directory + if (_marineTarballService == null) return false; + final String logDirectoryPath = payload['localLogPath']; + final jsonFilePath = p.join(logDirectoryPath, 'data.json'); + final file = File(jsonFilePath); + if (!await file.exists()) { + await _dbHelper.deleteRequestFromQueue(taskId); + return false; + } + final jsonData = jsonDecode(await file.readAsString()); + + // --- START: Manual Reconstruction of Tarball Data (Fixing .fromJson error) --- + final TarballSamplingData dataToResubmit = TarballSamplingData(); + dataToResubmit.firstSampler = jsonData['firstSampler']; + dataToResubmit.firstSamplerUserId = jsonData['firstSamplerUserId']; + dataToResubmit.secondSampler = jsonData['secondSampler']; + dataToResubmit.samplingDate = jsonData['samplingDate']; + dataToResubmit.samplingTime = jsonData['samplingTime']; + dataToResubmit.selectedStateName = jsonData['selectedStateName']; + dataToResubmit.selectedCategoryName = jsonData['selectedCategoryName']; + dataToResubmit.selectedStation = jsonData['selectedStation']; + dataToResubmit.stationLatitude = jsonData['stationLatitude']; + dataToResubmit.stationLongitude = jsonData['stationLongitude']; + dataToResubmit.currentLatitude = jsonData['currentLatitude']; + dataToResubmit.currentLongitude = jsonData['currentLongitude']; + dataToResubmit.distanceDifference = (jsonData['distanceDifference'] as num?)?.toDouble(); + dataToResubmit.distanceDifferenceRemarks = jsonData['distanceDifferenceRemarks']; + dataToResubmit.classificationId = jsonData['classificationId']; + dataToResubmit.selectedClassification = jsonData['selectedClassification']; + dataToResubmit.optionalRemark1 = jsonData['optionalRemark1']; + dataToResubmit.optionalRemark2 = jsonData['optionalRemark2']; + dataToResubmit.optionalRemark3 = jsonData['optionalRemark3']; + dataToResubmit.optionalRemark4 = jsonData['optionalRemark4']; + dataToResubmit.reportId = jsonData['reportId']; + dataToResubmit.submissionStatus = jsonData['submissionStatus']; + dataToResubmit.submissionMessage = jsonData['submissionMessage']; + + // Helper to create File from path if it exists + File? fileFromPath(dynamic path) => (path is String && path.isNotEmpty) ? File(path) : null; + + dataToResubmit.leftCoastalViewImage = fileFromPath(jsonData['leftCoastalViewImage']); + dataToResubmit.rightCoastalViewImage = fileFromPath(jsonData['rightCoastalViewImage']); + dataToResubmit.verticalLinesImage = fileFromPath(jsonData['verticalLinesImage']); + dataToResubmit.horizontalLineImage = fileFromPath(jsonData['horizontalLineImage']); + dataToResubmit.optionalImage1 = fileFromPath(jsonData['optionalImage1']); + dataToResubmit.optionalImage2 = fileFromPath(jsonData['optionalImage2']); + dataToResubmit.optionalImage3 = fileFromPath(jsonData['optionalImage3']); + dataToResubmit.optionalImage4 = fileFromPath(jsonData['optionalImage4']); + // --- END: Manual Reconstruction --- + + final result = await _marineTarballService!.submitTarballSample( + data: dataToResubmit, + appSettings: _authProvider!.appSettings, + context: null, + logDirectory: logDirectoryPath, + ); + success = result['success']; + + // ======================================================================= + // MARINE REPORT HANDLERS + // ======================================================================= + + } else if (taskType == 'marine_npe_submission' || taskType == 'npe_submission') { + if (_marineNpeService == null) return false; + final String logDirectoryPath = payload['localLogPath']; final jsonFilePath = p.join(logDirectoryPath, 'data.json'); final file = File(jsonFilePath); - if (!await file.exists()) { - debugPrint("Retry failed: Source log file no longer exists at $jsonFilePath"); await _dbHelper.deleteRequestFromQueue(taskId); return false; } final content = await file.readAsString(); final jsonData = jsonDecode(content) as Map; + final MarineManualNpeReportData dataToResubmit = MarineManualNpeReportData(); - // Recreate File objects from paths - File? fileFromJson(dynamic path) => (path is String && path.isNotEmpty) ? File(path) : null; + // Reconstruction logic + dataToResubmit.firstSamplerName = jsonData['firstSamplerName']; + dataToResubmit.firstSamplerUserId = jsonData['firstSamplerUserId']; + dataToResubmit.eventDate = jsonData['eventDate']; + dataToResubmit.eventTime = jsonData['eventTime']; + dataToResubmit.sourceOrigin = jsonData['sourceOrigin']; + dataToResubmit.locationDescription = jsonData['locationDescription']; + dataToResubmit.stateName = jsonData['stateName']; + dataToResubmit.selectedStation = jsonData['selectedStation']; + dataToResubmit.latitude = jsonData['latitude']; + dataToResubmit.longitude = jsonData['longitude']; + dataToResubmit.oxygenSaturation = jsonData['oxygenSaturation']; + dataToResubmit.electricalConductivity = jsonData['electricalConductivity']; + dataToResubmit.oxygenConcentration = jsonData['oxygenConcentration']; + dataToResubmit.turbidity = jsonData['turbidity']; + dataToResubmit.ph = jsonData['ph']; + dataToResubmit.temperature = jsonData['temperature']; + if (jsonData['fieldObservations'] != null) { + dataToResubmit.fieldObservations = Map.from(jsonData['fieldObservations']); + } + dataToResubmit.othersObservationRemark = jsonData['othersObservationRemark']; + dataToResubmit.possibleSource = jsonData['possibleSource']; + dataToResubmit.image1Remark = jsonData['image1Remark']; + dataToResubmit.image2Remark = jsonData['image2Remark']; + dataToResubmit.image3Remark = jsonData['image3Remark']; + dataToResubmit.image4Remark = jsonData['image4Remark']; + dataToResubmit.tarballClassificationId = jsonData['tarballClassificationId']; + dataToResubmit.selectedTarballClassification = jsonData['selectedTarballClassification']; + dataToResubmit.reportId = jsonData['reportId']; - final TarballSamplingData dataToResubmit = TarballSamplingData() - // Reconstruct the object from JSON data - ..firstSampler = jsonData['firstSampler'] - ..firstSamplerUserId = jsonData['firstSamplerUserId'] - ..secondSampler = jsonData['secondSampler'] - ..samplingDate = jsonData['samplingDate'] - ..samplingTime = jsonData['samplingTime'] - ..selectedStateName = jsonData['selectedStateName'] - ..selectedCategoryName = jsonData['selectedCategoryName'] - ..selectedStation = jsonData['selectedStation'] - ..stationLatitude = jsonData['stationLatitude'] - ..stationLongitude = jsonData['stationLongitude'] - ..currentLatitude = jsonData['currentLatitude'] - ..currentLongitude = jsonData['currentLongitude'] - ..distanceDifference = jsonData['distanceDifference'] is num ? (jsonData['distanceDifference'] as num).toDouble() : null // Safe cast - ..distanceDifferenceRemarks = jsonData['distanceDifferenceRemarks'] - ..classificationId = jsonData['classificationId'] is num ? (jsonData['classificationId'] as num).toInt() : null // Safe cast - ..selectedClassification = jsonData['selectedClassification'] - ..leftCoastalViewImage = fileFromJson(jsonData['leftCoastalViewImage']) - ..rightCoastalViewImage = fileFromJson(jsonData['rightCoastalViewImage']) - ..verticalLinesImage = fileFromJson(jsonData['verticalLinesImage']) - ..horizontalLineImage = fileFromJson(jsonData['horizontalLineImage']) - ..optionalImage1 = fileFromJson(jsonData['optionalImage1']) - ..optionalRemark1 = jsonData['optionalRemark1'] - ..optionalImage2 = fileFromJson(jsonData['optionalImage2']) - ..optionalRemark2 = jsonData['optionalRemark2'] - ..optionalImage3 = fileFromJson(jsonData['optionalImage3']) - ..optionalRemark3 = jsonData['optionalRemark3'] - ..optionalImage4 = fileFromJson(jsonData['optionalImage4']) - ..optionalRemark4 = jsonData['optionalRemark4'] - ..reportId = jsonData['reportId'] // Preserve reportId if it exists - ..submissionStatus = jsonData['submissionStatus'] // Preserve status info - ..submissionMessage = jsonData['submissionMessage']; + if (jsonData['npe_image_1'] != null) dataToResubmit.image1 = File(jsonData['npe_image_1']); + if (jsonData['npe_image_2'] != null) dataToResubmit.image2 = File(jsonData['npe_image_2']); + if (jsonData['npe_image_3'] != null) dataToResubmit.image3 = File(jsonData['npe_image_3']); + if (jsonData['npe_image_4'] != null) dataToResubmit.image4 = File(jsonData['npe_image_4']); - - debugPrint("Retrying Tarball submission..."); - // Pass null for BuildContext, and the logDirectory path - final result = await _marineTarballService!.submitTarballSample( + final result = await _marineNpeService!.submitNpeReport( data: dataToResubmit, - appSettings: _authProvider!.appSettings, - context: null, // Pass null for BuildContext during retry - logDirectory: logDirectoryPath, // Pass the directory path for potential update + authProvider: _authProvider!, + logDirectory: logDirectoryPath, ); success = result['success']; - // --- Simple Task Handlers --- + // *** START: ADDED Pre-Departure Checklist *** + } else if (taskType == 'pre_departure_submission') { + if (_marinePreDepartureService == null) return false; + final String logDirectoryPath = payload['localLogPath']; + final jsonFilePath = p.join(logDirectoryPath, 'data.json'); + final file = File(jsonFilePath); + if (!await file.exists()) { + await _dbHelper.deleteRequestFromQueue(taskId); + return false; + } + + final content = await file.readAsString(); + final jsonData = jsonDecode(content) as Map; + final MarineManualPreDepartureChecklistData dataToResubmit = MarineManualPreDepartureChecklistData(); + + // Reconstruct Data from JSON + dataToResubmit.reporterName = jsonData['reporterName']; + dataToResubmit.reporterUserId = jsonData['reporterUserId']; + dataToResubmit.submissionDate = jsonData['submissionDate']; + dataToResubmit.location = jsonData['location']; + if (jsonData['checklistItems'] != null) { + dataToResubmit.checklistItems = Map.from(jsonData['checklistItems']); + } + if (jsonData['remarks'] != null) { + dataToResubmit.remarks = Map.from(jsonData['remarks']); + } + dataToResubmit.reportId = jsonData['reportId']; + + final result = await _marinePreDepartureService!.submitChecklist( + data: dataToResubmit, + authProvider: _authProvider!, + appSettings: _authProvider!.appSettings, + logDirectory: logDirectoryPath, + ); + success = result['success']; + // *** END: ADDED Pre-Departure Checklist *** + + // *** START: ADDED Sonde Calibration *** + } else if (taskType == 'sonde_calibration_submission') { + if (_marineSondeCalibrationService == null) return false; + final String logDirectoryPath = payload['localLogPath']; + final jsonFilePath = p.join(logDirectoryPath, 'data.json'); + final file = File(jsonFilePath); + if (!await file.exists()) { + await _dbHelper.deleteRequestFromQueue(taskId); + return false; + } + + final content = await file.readAsString(); + final jsonData = jsonDecode(content) as Map; + final MarineManualSondeCalibrationData dataToResubmit = MarineManualSondeCalibrationData(); + + // Reconstruct Data + dataToResubmit.calibratedByUserId = jsonData['calibratedByUserId']; + dataToResubmit.calibratedByUserName = jsonData['calibratedByUserName']; + dataToResubmit.sondeSerialNumber = jsonData['sondeSerialNumber']; + dataToResubmit.firmwareVersion = jsonData['firmwareVersion']; + dataToResubmit.korVersion = jsonData['korVersion']; + dataToResubmit.location = jsonData['location']; + dataToResubmit.startDateTime = jsonData['startDateTime']; + dataToResubmit.endDateTime = jsonData['endDateTime']; + + // Cast to double as JSON decodes numbers as int if no decimal places + dataToResubmit.ph7Mv = (jsonData['ph_7_mv'] as num?)?.toDouble(); + dataToResubmit.ph7Before = (jsonData['ph_7_before'] as num?)?.toDouble(); + dataToResubmit.ph7After = (jsonData['ph_7_after'] as num?)?.toDouble(); + dataToResubmit.ph10Mv = (jsonData['ph_10_mv'] as num?)?.toDouble(); + dataToResubmit.ph10Before = (jsonData['ph_10_before'] as num?)?.toDouble(); + dataToResubmit.ph10After = (jsonData['ph_10_after'] as num?)?.toDouble(); + dataToResubmit.condBefore = (jsonData['cond_before'] as num?)?.toDouble(); + dataToResubmit.condAfter = (jsonData['cond_after'] as num?)?.toDouble(); + dataToResubmit.doBefore = (jsonData['do_before'] as num?)?.toDouble(); + dataToResubmit.doAfter = (jsonData['do_after'] as num?)?.toDouble(); + dataToResubmit.turbidity0Before = (jsonData['turbidity_0_before'] as num?)?.toDouble(); + dataToResubmit.turbidity0After = (jsonData['turbidity_0_after'] as num?)?.toDouble(); + dataToResubmit.turbidity124Before = (jsonData['turbidity_124_before'] as num?)?.toDouble(); + dataToResubmit.turbidity124After = (jsonData['turbidity_124_after'] as num?)?.toDouble(); + + dataToResubmit.calibrationStatus = jsonData['calibration_status']; + dataToResubmit.remarks = jsonData['remarks']; + dataToResubmit.reportId = jsonData['reportId']; + + final result = await _marineSondeCalibrationService!.submitCalibration( + data: dataToResubmit, + authProvider: _authProvider!, + appSettings: _authProvider!.appSettings, + logDirectory: logDirectoryPath, + ); + success = result['success']; + // *** END: ADDED Sonde Calibration *** + + // *** START: ADDED Equipment Maintenance *** + } else if (taskType == 'equipment_maintenance_submission') { + if (_marineEquipmentMaintenanceService == null) return false; + final String logDirectoryPath = payload['localLogPath']; + final jsonFilePath = p.join(logDirectoryPath, 'data.json'); + final file = File(jsonFilePath); + if (!await file.exists()) { + await _dbHelper.deleteRequestFromQueue(taskId); + return false; + } + + final content = await file.readAsString(); + final jsonData = jsonDecode(content) as Map; + final MarineManualEquipmentMaintenanceData dataToResubmit = MarineManualEquipmentMaintenanceData(); + + // Reconstruct Data + dataToResubmit.conductedByUserId = jsonData['conductedByUserId']; + dataToResubmit.conductedByUserName = jsonData['conductedByUserName']; + dataToResubmit.maintenanceDate = jsonData['maintenanceDate']; + dataToResubmit.lastMaintenanceDate = jsonData['lastMaintenanceDate']; + dataToResubmit.scheduleMaintenance = jsonData['scheduleMaintenance']; + dataToResubmit.isReplacement = jsonData['isReplacement'] ?? false; + dataToResubmit.timeStart = jsonData['timeStart']; + dataToResubmit.timeEnd = jsonData['timeEnd']; + dataToResubmit.location = jsonData['location']; + + if (jsonData['ysiSondeChecks'] != null) { + dataToResubmit.ysiSondeChecks = Map.from(jsonData['ysiSondeChecks']); + } + dataToResubmit.ysiSondeComments = jsonData['ysiSondeComments']; + + // Handle nested maps for Sensor Checks + if (jsonData['ysiSensorChecks'] != null) { + dataToResubmit.ysiSensorChecks = (jsonData['ysiSensorChecks'] as Map).map( + (key, value) => MapEntry(key, Map.from(value)), + ); + } + dataToResubmit.ysiSensorComments = jsonData['ysiSensorComments']; + + // Handle nested maps for Replacements + if (jsonData['ysiReplacements'] != null) { + dataToResubmit.ysiReplacements = (jsonData['ysiReplacements'] as Map).map( + (key, value) => MapEntry(key, Map.from(value)), + ); + } + + if (jsonData['vanDornChecks'] != null) { + dataToResubmit.vanDornChecks = (jsonData['vanDornChecks'] as Map).map( + (key, value) => MapEntry(key, Map.from(value)), + ); + } + dataToResubmit.vanDornComments = jsonData['vanDornComments']; + dataToResubmit.vanDornCurrentSerial = jsonData['vanDornCurrentSerial']; + dataToResubmit.vanDornNewSerial = jsonData['vanDornNewSerial']; + + if (jsonData['vanDornReplacements'] != null) { + dataToResubmit.vanDornReplacements = (jsonData['vanDornReplacements'] as Map).map( + (key, value) => MapEntry(key, Map.from(value)), + ); + } + dataToResubmit.reportId = jsonData['reportId']; + + final result = await _marineEquipmentMaintenanceService!.submitMaintenanceReport( + data: dataToResubmit, + authProvider: _authProvider!, + appSettings: _authProvider!.appSettings, + logDirectory: logDirectoryPath, + ); + success = result['success']; + // *** END: ADDED Equipment Maintenance *** + + // ======================================================================= + // SIMPLE API / FTP HANDLERS + // ======================================================================= + } else if (taskType == 'api') { final endpoint = task['endpoint_or_path'] as String; final method = payload['method'] as String; - final baseUrl = await _serverConfigService.getActiveApiUrl(); // Get current active URL - debugPrint("Retrying API task $taskId: $method to $baseUrl/$endpoint"); - Map result; + final baseUrl = await _serverConfigService.getActiveApiUrl(); if (method == 'POST_MULTIPART') { final Map fields = Map.from(payload['fields'] ?? {}); - // Recreate File objects from paths stored in the payload final Map files = (payload['files'] as Map?) ?.map((key, value) => MapEntry(key, File(value as String))) ?? {}; - // Check if files still exist before attempting upload bool allFilesExist = true; - List missingFiles = []; // Keep track of missing files for (var entry in files.entries) { - File file = entry.value; - if (!await file.exists()) { - debugPrint("Retry failed for API task $taskId: File ${file.path} (key: ${entry.key}) no longer exists."); + if (!await entry.value.exists()) { allFilesExist = false; - missingFiles.add(entry.key); - // break; // Stop checking further if one is missing } } - - // If some files are missing, fail the entire task. if (!allFilesExist) { - debugPrint("API Multipart retry failed for task $taskId because files are missing: ${missingFiles.join(', ')}. Removing task."); - await _dbHelper.deleteRequestFromQueue(taskId); // Remove invalid task + await _dbHelper.deleteRequestFromQueue(taskId); return false; } - - result = await _baseApiService.postMultipart(baseUrl: baseUrl, endpoint: endpoint, fields: fields, files: files); - } else { // Standard POST + final result = await _baseApiService.postMultipart(baseUrl: baseUrl, endpoint: endpoint, fields: fields, files: files); + success = result['success']; + } else { final Map body = Map.from(payload['body'] ?? {}); - result = await _baseApiService.post(baseUrl, endpoint, body); + final result = await _baseApiService.post(baseUrl, endpoint, body); + success = result['success']; } - success = result['success']; } else if (taskType == 'ftp') { final remotePath = task['endpoint_or_path'] as String; final localFile = File(payload['localFilePath'] as String); final int? ftpConfigId = payload['ftpConfigId'] as int?; - debugPrint("Retrying FTP task $taskId: Uploading ${localFile.path} to $remotePath using config ID $ftpConfigId"); - if (ftpConfigId == null) { - debugPrint("Retry failed for FTP task $taskId: Missing FTP configuration ID in payload."); - await _dbHelper.deleteRequestFromQueue(taskId); // Remove invalid task + await _dbHelper.deleteRequestFromQueue(taskId); + return false; + } + if (await localFile.exists()) { + final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? []; + final config = ftpConfigs.firstWhere((c) => c['ftp_config_id'] == ftpConfigId, orElse: () => {}); + if (config.isEmpty) return false; + + final result = await _ftpService.uploadFile(config: config, fileToUpload: localFile, remotePath: remotePath); + success = result['success']; + } else { + await _dbHelper.deleteRequestFromQueue(taskId); return false; } - if (await localFile.exists()) { - final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? []; - final config = ftpConfigs.firstWhere((c) => c['ftp_config_id'] == ftpConfigId, orElse: () => {}); // Use explicit type - - - if (config.isEmpty) { - debugPrint("Retry failed for FTP task $taskId: FTP configuration with ID $ftpConfigId not found."); - return false; // Fail the retry attempt, keep in queue - } - - // Attempt upload using the specific config - final result = await _ftpService.uploadFile(config: config, fileToUpload: localFile, remotePath: remotePath); - success = result['success']; - - } else { - debugPrint("Retry failed for FTP task $taskId: Source file no longer exists at ${localFile.path}"); - await _dbHelper.deleteRequestFromQueue(taskId); // Remove task if file is gone - return false; // Explicitly return false as success is false - } } else { debugPrint("Unknown task type '$taskType' for task ID $taskId. Cannot retry. Removing task."); await _dbHelper.deleteRequestFromQueue(taskId); } } on SessionExpiredException catch (e) { - debugPrint("Session expired during retry attempt for task $taskId (Type: $taskType): $e. Task remains in queue."); - success = false; // Session expiry during retry means failure for this attempt - } catch (e, stacktrace) { // Catch potential exceptions during processing - debugPrint("A critical error occurred while retrying task $taskId (Type: $taskType): $e"); - debugPrint("Stacktrace: $stacktrace"); // Log stacktrace for detailed debugging - success = false; // Ensure success is false on exception + debugPrint("Session expired during retry attempt: $e"); + success = false; + } catch (e, stacktrace) { + debugPrint("A critical error occurred while retrying task $taskId: $e"); + debugPrint("Stacktrace: $stacktrace"); + success = false; } - // Post-processing: Remove successful tasks from queue if (success) { debugPrint("Task $taskId (Type: $taskType) completed successfully. Removing from queue."); await _dbHelper.deleteRequestFromQueue(taskId); - // If it was a complex task involving temporary ZIP files, attempt to delete them + if (taskType.endsWith('_submission') && payload['localLogPath'] != null) { - // Assume localLogPath points to the JSON file, get directory for cleanup String pathToCheck = payload['localLogPath']; - // Check if it's a directory path already (for older marine insitu logs) bool isDirectory = await Directory(pathToCheck).exists(); if (!isDirectory && pathToCheck.endsWith('.json')) { - pathToCheck = p.dirname(pathToCheck); // Get directory if it's a file path - isDirectory = true; // Now we are checking the directory + pathToCheck = p.dirname(pathToCheck); + isDirectory = true; } _cleanUpTemporaryZipFiles(pathToCheck, isDirectory: isDirectory); } - // If it was an FTP task, attempt to delete the temporary ZIP file + if (taskType == 'ftp' && payload['localFilePath'] != null && (payload['localFilePath'] as String).endsWith('.zip')) { _cleanUpTemporaryZipFiles(payload['localFilePath'], isDirectory: false); } } else { debugPrint("Retry attempt for task $taskId (Type: $taskType) failed. It will remain in the queue."); - // Optional: Implement a retry limit here. If retries > X, mark task as 'failed' instead of 'pending'. - // e.g., await _dbHelper.updateTaskStatus(taskId, 'failed'); } return success; } - /// Helper function to delete temporary zip files after successful retry. void _cleanUpTemporaryZipFiles(String path, {required bool isDirectory}) async { try { if (isDirectory) { @@ -511,31 +659,17 @@ class RetryService { if (await dir.exists()) { final filesInDir = dir.listSync(); for (var entity in filesInDir) { - // Delete only ZIP files within the log directory if (entity is File && entity.path.endsWith('.zip')) { debugPrint("Deleting temporary zip file from directory: ${entity.path}"); await entity.delete(); } } - // Optional: Delete the directory itself if now empty, ONLY if safe. - // Be cautious as data.json might still be needed or other files exist. - // if (await dir.listSync().isEmpty) { - // await dir.delete(); - // debugPrint("Deleted empty log directory: ${dir.path}"); - // } - } else { - debugPrint("Log directory not found for cleanup: $path"); } } else { - // If it's a specific file path (like from FTP task) final file = File(path); - if (await file.exists() && path.endsWith('.zip')) { // Ensure it's a zip file + if (await file.exists() && path.endsWith('.zip')) { debugPrint("Deleting temporary zip file: ${file.path}"); await file.delete(); - } else if (!path.endsWith('.zip')) { - debugPrint("Skipping cleanup for non-zip file path: $path"); - } else { - debugPrint("Temporary zip file not found for cleanup: $path"); } } } catch (e) { @@ -543,4 +677,4 @@ class RetryService { } } -} // End of RetryService class \ No newline at end of file +} \ No newline at end of file diff --git a/lib/services/river_investigative_sampling_service.dart b/lib/services/river_investigative_sampling_service.dart index 263b93a..1fc3d6f 100644 --- a/lib/services/river_investigative_sampling_service.dart +++ b/lib/services/river_investigative_sampling_service.dart @@ -540,61 +540,24 @@ class RiverInvestigativeSamplingService { // Renamed class } // --- START: NEW HELPER METHOD (for timestamp ID) --- - /// Generates the specific Telegram alert message content for River Investigative. - Future _generateInvestigativeAlertMessage(RiverInvesManualSamplingData data, {required bool isDataOnly}) async { // Updated model type - final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)"; - // Use helpers to get determined names/codes - final stationName = data.getDeterminedRiverName() ?? data.getDeterminedStationName() ?? 'N/A'; // Combine river/station name - final stationCode = data.getDeterminedStationCode() ?? 'N/A'; - final submissionDate = data.samplingDate ?? DateFormat('yyyy-MM-dd').format(DateTime.now()); - final submitter = data.firstSamplerName ?? 'N/A'; - final sondeID = data.sondeId ?? 'N/A'; - final distanceKm = data.distanceDifferenceInKm ?? 0; - final distanceMeters = (distanceKm * 1000).toStringAsFixed(0); - final distanceRemarks = data.distanceDifferenceRemarks ?? ''; // Default to empty string - - final buffer = StringBuffer() - ..writeln('✅ *River Investigative Sample ${submissionType} Submitted:*') // Updated title - ..writeln(); - - // Adapt station info based on type - buffer.writeln('*Station Type:* ${data.stationTypeSelection ?? 'N/A'}'); - if (data.stationTypeSelection == 'New Location') { - buffer.writeln('*New Location Name:* ${data.newStationName ?? 'N/A'}'); - buffer.writeln('*New Location Code:* ${data.newStationCode ?? 'N/A'}'); - buffer.writeln('*New Location State:* ${data.newStateName ?? 'N/A'}'); - buffer.writeln('*New Location Basin:* ${data.newBasinName ?? 'N/A'}'); - buffer.writeln('*New Location River:* ${data.newRiverName ?? 'N/A'}'); - buffer.writeln('*Coordinates:* ${data.stationLatitude ?? 'N/A'}, ${data.stationLongitude ?? 'N/A'}'); - } else { - buffer.writeln('*Station Name & Code:* $stationName ($stationCode)'); + /// Generates a unique timestamp ID from the sampling date and time. + // Note: This function was duplicated. The duplicate has been removed. + // The first occurrence of this function is kept, even though the error message pointed to it. + // Keeping this one: + /* + String _generateTimestampId(String? date, String? time) { + final String dateTimeString = "${date ?? ''} ${time ?? ''}"; + try { + // Time format from model is HH:mm + final DateTime samplingDateTime = DateFormat('yyyy-MM-dd HH:mm').parse(dateTimeString); + return samplingDateTime.millisecondsSinceEpoch.toString(); + } catch (e) { + // Fallback: if parsing fails, use the current time in milliseconds + debugPrint("Could not parse '$dateTimeString' for timestamp ID, using current time. Error: $e"); + return DateTime.now().millisecondsSinceEpoch.toString(); } - - buffer - ..writeln('*Date of Submitted:* $submissionDate') - ..writeln('*Submitted by User:* $submitter') - ..writeln('*Sonde ID:* $sondeID') - ..writeln('*Status of Submission:* Successful'); - - // Include distance warning only if NOT a new location and distance > 50m - if (data.stationTypeSelection != 'New Location' && (distanceKm * 1000 > 50 || distanceRemarks.isNotEmpty)) { - buffer - ..writeln() - ..writeln('🔔 *Distance Alert:*') - ..writeln('*Distance from station:* $distanceMeters meters'); - if (distanceRemarks.isNotEmpty) { - buffer.writeln('*Remarks for distance:* $distanceRemarks'); - } - } - - // Add parameter limit check section (uses the same river limits) - final outOfBoundsAlert = await _getOutOfBoundsAlertSection(data); // Call helper - if (outOfBoundsAlert.isNotEmpty) { - buffer.write(outOfBoundsAlert); - } - - return buffer.toString(); } + */ // --- END: NEW HELPER METHOD --- // --- START: MODIFIED _generateBaseFileName --- @@ -635,6 +598,7 @@ class RiverInvestigativeSamplingService { // Renamed class // 4. CREATE DATA ZIP final dataZip = await _zippingService.createDataZip( + // --- START FIX: Include all four JSON files --- jsonDataMap: { // *** MODIFIED: Use Investigative model's JSON methods and filenames *** 'db.json': data.toDbJson(), // Main data structure @@ -642,6 +606,7 @@ class RiverInvestigativeSamplingService { // Renamed class 'river_inves_reading.json': data.toReadingJson(), 'river_inves_manual_info.json': data.toManualInfoJson(), }, + // --- END FIX --- baseFileName: baseFileName, destinationDir: localSubmissionDir, // Save ZIP in the specific log folder ); @@ -786,7 +751,9 @@ class RiverInvestigativeSamplingService { // Renamed class /// Handles sending or queuing the Telegram alert for River Investigative submissions. Future _handleSuccessAlert(RiverInvesManualSamplingData data, List>? appSettings, {required bool isDataOnly, bool isSessionExpired = false}) async { // Updated model type try { - final message = await _generateInvestigativeAlertMessage(data, isDataOnly: isDataOnly); // Call specific helper + // --- FIX: Correct function name to the defined helper method --- + final message = await _generateSuccessAlertMessage(data, isDataOnly: isDataOnly); // Call specific helper + // --- END FIX --- // *** MODIFIED: Telegram key *** final alertKey = 'river_investigative'; // Specific key for this module diff --git a/lib/services/river_manual_triennial_sampling_service.dart b/lib/services/river_manual_triennial_sampling_service.dart index aab9723..2fca90c 100644 --- a/lib/services/river_manual_triennial_sampling_service.dart +++ b/lib/services/river_manual_triennial_sampling_service.dart @@ -308,6 +308,35 @@ class RiverManualTriennialSamplingService { await _retryService.addApiToQueue(endpoint: 'river/triennial/images', method: 'POST_MULTIPART', fields: {'r_tri_id': apiRecordId}, files: finalImageFiles); // --- END: MODIFIED TO USE TIMESTAMP ID --- } + + // --- START FIX: Queue all four JSON files --- + // Get all potential FTP configs + final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? []; + + final dataZip = await _zippingService.createDataZip( + jsonDataMap: { // Use specific JSON structures for River Triennial FTP + 'db.json': data.toDbJson(), // Assuming similar structure is needed, adjust if different + 'river_triennial_basic_form.json': data.toBasicFormJson(), + 'river_triennial_reading.json': data.toReadingJson(), + 'river_triennial_manual_info.json': data.toManualInfoJson(), + }, + baseFileName: _generateBaseFileName(data), + destinationDir: null, + ); + if (dataZip != null) { + // Queue for each config separately + for (final config in ftpConfigs) { + final configId = config['ftp_config_id']; + if (configId != null) { + await _retryService.addFtpToQueue( + localFilePath: dataZip.path, + remotePath: '/${p.basename(dataZip.path)}', + ftpConfigId: configId // Provide the specific config ID + ); + } + } + } + // --- END FIX --- } // 3. Submit FTP Files @@ -327,7 +356,9 @@ class RiverManualTriennialSamplingService { final dataZip = await _zippingService.createDataZip( jsonDataMap: { // Use specific JSON structures for River Triennial FTP 'db.json': data.toDbJson(), // Assuming similar structure is needed, adjust if different - // Add other JSON files if required for Triennial FTP + 'river_triennial_basic_form.json': data.toBasicFormJson(), // ADDED + 'river_triennial_reading.json': data.toReadingJson(), // ADDED + 'river_triennial_manual_info.json': data.toManualInfoJson(), // ADDED }, baseFileName: baseFileNameForQueue, destinationDir: null, @@ -537,7 +568,14 @@ class RiverManualTriennialSamplingService { // 4. CREATE DATA ZIP final dataZip = await _zippingService.createDataZip( - jsonDataMap: {'db.json': data.toDbJson()}, // Assuming similar structure, adjust if needed + // --- START FIX: Include all four JSON files --- + jsonDataMap: { + 'db.json': data.toDbJson(), + 'river_triennial_basic_form.json': data.toBasicFormJson(), // ADDED + 'river_triennial_reading.json': data.toReadingJson(), // ADDED + 'river_triennial_manual_info.json': data.toManualInfoJson(), // ADDED + }, + // --- END FIX --- baseFileName: baseFileName, destinationDir: localSubmissionDir, ); diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index e045ee7..7b09e1d 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -1,6 +1,6 @@ -import 'package:flutter/foundation.dart'; +// lib/services/settings_service.dart -// No longer needs SharedPreferences or BaseApiService for its core logic. +import 'package:flutter/foundation.dart'; class SettingsService { // The service no longer manages its own state or makes API calls. @@ -32,43 +32,55 @@ class SettingsService { return ''; } - /// Gets the Chat ID for the Marine In-Situ module from the provided settings list. + /// Gets the Chat ID for the Marine In-Situ module. String getInSituChatId(List>? settings) { return _getChatId(settings, 'marine_in_situ'); } - /// Gets the Chat ID for the Tarball module from the provided settings list. + /// Gets the Chat ID for the Tarball module. String getTarballChatId(List>? settings) { return _getChatId(settings, 'marine_tarball'); } - /// Gets the Chat ID for the River In-Situ module from the provided settings list. + /// Gets the Chat ID for the Marine Investigative module. + String getMarineInvestigativeChatId(List>? settings) { + return _getChatId(settings, 'marine_investigative'); + } + + // --- ADDED: Getter for Marine NPE Report --- + String getMarineReportChatId(List>? settings) { + return _getChatId(settings, 'marine_report'); + } + // ------------------------------------------ + + /// Gets the Chat ID for the River In-Situ module. String getRiverInSituChatId(List>? settings) { return _getChatId(settings, 'river_in_situ'); } - /// Gets the Chat ID for the River Triennial module from the provided settings list. + /// Gets the Chat ID for the River Triennial module. String getRiverTriennialChatId(List>? settings) { return _getChatId(settings, 'river_triennial'); } - /// Gets the Chat ID for the River Investigative module from the provided settings list. + /// Gets the Chat ID for the River Investigative module. String getRiverInvestigativeChatId(List>? settings) { return _getChatId(settings, 'river_investigative'); } - /// Gets the Chat ID for the Air Manual module from the provided settings list. + // --- ADDED: Getter for River NPE Report --- + String getRiverReportChatId(List>? settings) { + return _getChatId(settings, 'river_report'); + } + // ------------------------------------------ + + /// Gets the Chat ID for the Air Manual module. String getAirManualChatId(List>? settings) { return _getChatId(settings, 'air_manual'); } - /// Gets the Chat ID for the Air Investigative module from the provided settings list. + /// Gets the Chat ID for the Air Investigative module. String getAirInvestigativeChatId(List>? settings) { return _getChatId(settings, 'air_investigative'); } - - /// Gets the Chat ID for the Marine Investigative module from the provided settings list. - String getMarineInvestigativeChatId(List>? settings) { - return _getChatId(settings, 'marine_investigative'); - } } \ No newline at end of file