// lib/main.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'dart:async'; // Import Timer import 'package:provider/single_child_widget.dart'; import 'package:environment_monitoring_app/services/api_service.dart'; import 'package:environment_monitoring_app/services/database_helper.dart'; import 'package:environment_monitoring_app/services/local_storage_service.dart'; import 'package:environment_monitoring_app/services/river_in_situ_sampling_service.dart'; import 'package:environment_monitoring_app/services/river_manual_triennial_sampling_service.dart'; 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'; import 'package:environment_monitoring_app/services/server_config_service.dart'; import 'package:environment_monitoring_app/services/retry_service.dart'; import 'package:environment_monitoring_app/services/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'; 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'; import 'package:environment_monitoring_app/theme.dart'; import 'package:environment_monitoring_app/auth_provider.dart'; // Core Screens import 'package:environment_monitoring_app/screens/login.dart'; import 'package:environment_monitoring_app/screens/register.dart'; import 'package:environment_monitoring_app/screens/forgot_password.dart'; import 'package:environment_monitoring_app/screens/logout.dart'; import 'package:environment_monitoring_app/home_page.dart'; import 'package:environment_monitoring_app/screens/profile.dart'; import 'package:environment_monitoring_app/screens/settings.dart'; // 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'; // Department Home Pages import 'package:environment_monitoring_app/screens/air/air_home_page.dart'; import 'package:environment_monitoring_app/screens/river/river_home_page.dart'; import 'package:environment_monitoring_app/screens/marine/marine_home_page.dart'; // Air Screens import 'package:environment_monitoring_app/screens/air/manual/air_manual_info_centre_document.dart'; import 'package:environment_monitoring_app/screens/air/manual/air_manual_installation_screen.dart'; import 'package:environment_monitoring_app/screens/air/manual/air_manual_collection_screen.dart'; import 'package:environment_monitoring_app/screens/air/manual/air_manual_report.dart' as airManualReport; import 'package:environment_monitoring_app/screens/air/manual/air_manual_data_status_log.dart' as airManualDataStatusLog; import 'package:environment_monitoring_app/screens/air/manual/air_manual_image_request.dart' as airManualImageRequest; import 'package:environment_monitoring_app/screens/air/continuous/air_continuous_info_centre_document.dart'; import 'package:environment_monitoring_app/screens/air/continuous/overview.dart' as airContinuousOverview; import 'package:environment_monitoring_app/screens/air/continuous/entry.dart' as airContinuousEntry; import 'package:environment_monitoring_app/screens/air/continuous/report.dart' as airContinuousReport; import 'package:environment_monitoring_app/screens/air/investigative/air_investigative_info_centre_document.dart'; import 'package:environment_monitoring_app/screens/air/investigative/overview.dart' as airInvestigativeOverview; import 'package:environment_monitoring_app/screens/air/investigative/entry.dart' as airInvestigativeEntry; import 'package:environment_monitoring_app/screens/air/investigative/report.dart' as airInvestigativeReport; // River Screens import 'package:environment_monitoring_app/screens/river/manual/river_manual_info_centre_document.dart'; import 'package:environment_monitoring_app/screens/river/manual/in_situ_sampling.dart' as riverManualInSituSampling; import 'package:environment_monitoring_app/screens/river/manual/river_manual_data_status_log.dart' as riverManualDataStatusLog; import 'package:environment_monitoring_app/screens/river/manual/river_manual_report.dart' as riverManualReport; import 'package:environment_monitoring_app/screens/river/manual/triennial/river_manual_triennial_sampling.dart' as riverManualTriennialSampling; import 'package:environment_monitoring_app/screens/river/manual/river_manual_image_request.dart' as riverManualImageRequest; import 'package:environment_monitoring_app/screens/river/continuous/river_continuous_info_centre_document.dart'; import 'package:environment_monitoring_app/screens/river/continuous/overview.dart' as riverContinuousOverview; 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'; import 'package:environment_monitoring_app/screens/river/investigative/river_investigative_manual_sampling.dart' as riverInvestigativeManualSampling; 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; // Marine Screens import 'package:environment_monitoring_app/screens/marine/manual/info_centre_document.dart' as marineManualInfoCentreDocument; import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_pre_sampling.dart' as marineManualPreSampling; 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; 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; 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; import 'package:environment_monitoring_app/models/tarball_data.dart'; import 'package:environment_monitoring_app/screens/marine/manual/tarball_sampling_step1.dart'; import 'package:environment_monitoring_app/screens/marine/manual/tarball_sampling_step2.dart'; import 'package:environment_monitoring_app/screens/marine/manual/tarball_sampling_step3_summary.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Create singleton instances of core services before running the app final DatabaseHelper databaseHelper = DatabaseHelper(); 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); 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. final authProvider = AuthProvider( apiService: apiService, dbHelper: databaseHelper, serverConfigService: ServerConfigService(), retryService: retryService, ); // Initialize the retry service with all its dependencies. retryService.initialize( marineInSituService: marineInSituService, riverInSituService: riverInSituService, marineInvestigativeService: marineInvestigativeService, 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, ); setupPeriodicServices(telegramService, retryService); runApp( MultiProvider( providers: [ ChangeNotifierProvider.value(value: authProvider), Provider(create: (_) => apiService), Provider(create: (_) => databaseHelper), Provider(create: (_) => telegramService), Provider(create: (_) => LocalStorageService()), Provider.value(value: retryService), // Sampling Services Provider.value(value: marineInSituService), Provider.value(value: marineInvestigativeService), Provider.value(value: riverInSituService), 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)), ], child: const RootApp(), ), ); } void setupPeriodicServices(TelegramService telegramService, RetryService retryService) { // Initial processing on startup (delayed) Future.delayed(const Duration(seconds: 5), () { debugPrint("[Main] Performing initial alert queue processing on app start."); telegramService.processAlertQueue(); debugPrint("[Main] Performing initial retry queue processing on app start."); retryService.processRetryQueue(); }); // Start recurring timers to process both queues every 5 minutes. Timer.periodic(const Duration(minutes: 5), (timer) { debugPrint("[Main] Periodic check: Processing Telegram alert queue..."); telegramService.processAlertQueue(); debugPrint("[Main] Periodic check: Processing main retry queue..."); retryService.processRetryQueue(); }); } class RootApp extends StatefulWidget { const RootApp({super.key}); @override State createState() => _RootAppState(); } class _RootAppState extends State { Timer? _configSyncTimer; @override void initState() { super.initState(); _initializeConnectivityListener(); _performInitialSessionCheck(); _initializePeriodicSync(); } @override void dispose() { _configSyncTimer?.cancel(); super.dispose(); } void _performInitialSessionCheck() async { await Future.delayed(const Duration(milliseconds: 100)); if (mounted) { final authProvider = Provider.of(context, listen: false); await authProvider.proactiveTokenRefresh(); final didTransition = await authProvider.checkAndTransitionToOnlineSession(); if (!didTransition) { authProvider.validateAndRefreshSession(); } } } void _initializeConnectivityListener() { Connectivity().onConnectivityChanged.listen((List results) { if (!results.contains(ConnectivityResult.none)) { debugPrint("[Main] Internet connection detected."); if (mounted) { final authProvider = Provider.of(context, listen: false); final telegramService = Provider.of(context, listen: false); final retryService = Provider.of(context, listen: false); authProvider.checkAndTransitionToOnlineSession().then((didTransition) { if (!didTransition) { authProvider.validateAndRefreshSession(); } }); telegramService.processAlertQueue(); retryService.processRetryQueue(); } } else { debugPrint("[Main] Internet connection lost."); } }); } void _initializePeriodicSync() { _configSyncTimer = Timer.periodic(const Duration(hours: 1), (timer) { debugPrint("[Main] Periodic 1-hour sync triggered."); if (mounted) { final authProvider = context.read(); if (authProvider.isLoggedIn && !authProvider.isSessionExpired) { debugPrint("[Main] User is logged in. Starting periodic data sync..."); authProvider.syncAllData().catchError((e) { debugPrint("[Main] Error during periodic 1-hour sync: $e"); }); } else { debugPrint("[Main] Skipping periodic sync: User not logged in or session expired."); } } }); } @override Widget build(BuildContext context) { return Consumer( builder: (context, auth, child) { Widget homeWidget; if (auth.isLoading) { homeWidget = const SplashScreen(); } else if (auth.isLoggedIn) { homeWidget = const SessionAwareWrapper(child: HomePage()); } else { homeWidget = const LoginScreen(); } return MaterialApp( title: 'Environment Monitoring App', theme: AppTheme.darkBlueTheme, debugShowCheckedModeBanner: false, home: homeWidget, onGenerateRoute: (settings) { if (settings.name == '/marine/manual/tarball/step2') { final args = settings.arguments as TarballSamplingData; return MaterialPageRoute(builder: (context) { return TarballSamplingStep2(data: args); }); } if (settings.name == '/marine/manual/tarball/step3') { final args = settings.arguments as TarballSamplingData; return MaterialPageRoute(builder: (context) { return TarballSamplingStep3Summary(data: args); }); } if (settings.name == '/marine/manual/data-log') { return MaterialPageRoute(builder: (context) { return const marineManualDataStatusLog.MarineManualDataStatusLog(); }); } return null; }, 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(), '/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/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/info': (context) => const AirContinuousInfoCentreDocument(), '/air/continuous/overview': (context) => airContinuousOverview.OverviewScreen(), '/air/continuous/entry': (context) => airContinuousEntry.EntryScreen(), '/air/continuous/report': (context) => airContinuousReport.ReportScreen(), '/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/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/data-log': (context) => riverManualDataStatusLog.RiverManualDataStatusLog(), '/river/manual/image-request': (context) => riverManualImageRequest.RiverManualImageRequest(), '/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/info': (context) => const RiverInvestigativeInfoCentreDocument(), '/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/image-request': (context) => const marineManualImageRequest.MarineImageRequestScreen(), '/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/info': (context) => const MarineInvestigativeInfoCentreDocument(), '/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(), }, ); }, ); } } class SessionAwareWrapper extends StatefulWidget { final Widget child; const SessionAwareWrapper({super.key, required this.child}); @override State createState() => _SessionAwareWrapperState(); } class _SessionAwareWrapperState extends State { bool _isDialogShowing = false; late AuthProvider _authProvider; @override void didChangeDependencies() { super.didChangeDependencies(); _authProvider = Provider.of(context); _authProvider.addListener(_handleSessionExpired); _checkAndShowDialogIfNeeded(_authProvider.isSessionExpired); } @override void dispose() { _authProvider.removeListener(_handleSessionExpired); super.dispose(); } void _handleSessionExpired() { _checkAndShowDialogIfNeeded(_authProvider.isSessionExpired); } void _checkAndShowDialogIfNeeded(bool isExpired) { if (isExpired && !_isDialogShowing && mounted) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && !_isDialogShowing) { _showSessionExpiredDialog(); } }); } } Future _showSessionExpiredDialog() async { if (!mounted) return; setState(() => _isDialogShowing = true); await showDialog( context: context, barrierDismissible: false, builder: (BuildContext dialogContext) { return AlertDialog( title: const Text("Session Expired"), content: const Text( "Your online session has expired. You can continue working offline, but you will not be able to sync data until you log in again."), actions: [ TextButton( child: const Text("Continue Offline"), onPressed: () { Navigator.of(dialogContext).pop(); }, ), ElevatedButton( child: const Text("Login Now"), onPressed: () { _authProvider.logout(); Navigator.of(dialogContext).pop(); }, ), ], ); }, ); if (mounted) { setState(() => _isDialogShowing = false); } } @override Widget build(BuildContext context) { return widget.child; } } class SplashScreen extends StatelessWidget { const SplashScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( 'assets/icon4.png', height: 360, width: 360, ), const SizedBox(height: 24), const CircularProgressIndicator(), const SizedBox(height: 20), const Text('Loading MMS data...', style: TextStyle(fontSize: 16)), ], ), ), ); } }