modfiy marine npe report to properly send api and telegram alert to server
This commit is contained in:
parent
cf22668576
commit
d0f9d72ebd
252
lib/main.dart
252
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<TelegramService>(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<TelegramService>(context, listen: false))),
|
||||
Provider(
|
||||
create: (context) =>
|
||||
MarineManualPreDepartureService(Provider.of<ApiService>(context, listen: false))),
|
||||
Provider(
|
||||
create: (context) =>
|
||||
MarineManualSondeCalibrationService(Provider.of<ApiService>(context, listen: false))),
|
||||
Provider(
|
||||
create: (context) =>
|
||||
MarineManualEquipmentMaintenanceService(Provider.of<ApiService>(context, listen: false))),
|
||||
],
|
||||
child: const RootApp(),
|
||||
),
|
||||
@ -232,67 +225,49 @@ class RootApp extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RootAppState extends State<RootApp> {
|
||||
// --- 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<AuthProvider>(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<ConnectivityResult> results) {
|
||||
if (!results.contains(ConnectivityResult.none)) {
|
||||
debugPrint("[Main] Internet connection detected.");
|
||||
if (mounted) {
|
||||
// Access services from provider context
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
final telegramService = Provider.of<TelegramService>(context, listen: false);
|
||||
final retryService = Provider.of<RetryService>(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<RootApp> {
|
||||
});
|
||||
}
|
||||
|
||||
// --- 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<AuthProvider>();
|
||||
|
||||
// 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<RootApp> {
|
||||
}
|
||||
});
|
||||
}
|
||||
// --- END: MODIFICATION ---
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -348,7 +313,6 @@ class _RootAppState extends State<RootApp> {
|
||||
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<RootApp> {
|
||||
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<SessionAwareWrapper> {
|
||||
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<AuthProvider>(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<SessionAwareWrapper> {
|
||||
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<SessionAwareWrapper> {
|
||||
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,
|
||||
),
|
||||
|
||||
@ -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<String, dynamic>? 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',
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -166,6 +166,16 @@ class _NPEReportFromInSituState extends State<NPEReportFromInSitu> {
|
||||
_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<NPEReportFromInSitu> {
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
final service = Provider.of<MarineNpeReportService>(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<NPEReportFromInSitu> {
|
||||
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),
|
||||
// -----------------------------------------------------------------------------------------
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -182,7 +182,7 @@ class _NPEReportFromTarballState extends State<NPEReportFromTarball> {
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
final service = Provider.of<MarineNpeReportService>(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] : '';
|
||||
|
||||
@ -172,7 +172,7 @@ class _NPEReportNewLocationState extends State<NPEReportNewLocation> {
|
||||
final auth = Provider.of<AuthProvider>(context, listen: false);
|
||||
final service = Provider.of<MarineNpeReportService>(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] : '';
|
||||
|
||||
@ -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<Map<String, dynamic>> Function()
|
||||
onSubmit; // Expects a function that returns the submission result
|
||||
final Future<Map<String, dynamic>> Function() onSubmit; // Expects a function that returns the submission result
|
||||
final bool isLoading;
|
||||
|
||||
const InSituStep4Summary({
|
||||
@ -43,11 +42,20 @@ class _InSituStep4SummaryState extends State<InSituStep4Summary> {
|
||||
'batteryVoltage': 'Battery',
|
||||
};
|
||||
|
||||
// --- START: FIXED OUT OF BOUNDS LOGIC ---
|
||||
Set<String> _getOutOfBoundsKeys(BuildContext context) {
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
final marineLimits = authProvider.marineParameterLimits ?? [];
|
||||
final Set<String> 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<InSituStep4Summary> {
|
||||
final limitName = _parameterKeyToLimitName[key];
|
||||
if (limitName == null) return;
|
||||
|
||||
Map<String, dynamic> 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<InSituStep4Summary> {
|
||||
|
||||
return invalidKeys;
|
||||
}
|
||||
// --- END: FIXED OUT OF BOUNDS LOGIC ---
|
||||
|
||||
/// Checks captured data against NPE limits and returns detailed information for the dialog.
|
||||
List<Map<String, dynamic>> _getNpeTriggeredParameters(BuildContext context) {
|
||||
@ -136,7 +138,6 @@ class _InSituStep4SummaryState extends State<InSituStep4Summary> {
|
||||
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<InSituStep4Summary> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles the complete submission flow: NPE check, submission, and UI feedback/navigation.
|
||||
Future<void> _handleSubmit(BuildContext context) async {
|
||||
if (_isHandlingSubmit || widget.isLoading) return;
|
||||
|
||||
@ -327,14 +327,12 @@ class _InSituStep4SummaryState extends State<InSituStep4Summary> {
|
||||
} 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<InSituStep4Summary> {
|
||||
|
||||
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<InSituStep4Summary> {
|
||||
|
||||
@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<InSituStep4Summary> {
|
||||
_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<InSituStep4Summary> {
|
||||
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<Uint8List>(
|
||||
@ -667,7 +665,6 @@ class _InSituStep4SummaryState extends State<InSituStep4Summary> {
|
||||
},
|
||||
),
|
||||
)
|
||||
// --- END MODIFICATION ---
|
||||
else
|
||||
Container(
|
||||
height: 100,
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -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');
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// lib/services/marine_npe_report_service.dart
|
||||
// lib/services/marine_tarball_sampling_service.dart
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
@ -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<void> 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<String, String>? fields,
|
||||
Map<String, File>? 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<void> 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<List<Map<String, dynamic>>> getPendingTasks() {
|
||||
return _dbHelper.getPendingRequests();
|
||||
}
|
||||
|
||||
/// Processes the entire queue of pending tasks.
|
||||
Future<void> 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<bool> 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<String, dynamic> payload; // Declare outside try-catch
|
||||
final String taskType = task['type'] as String; // Get type early for logging
|
||||
Map<String, dynamic> 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<String, dynamic>;
|
||||
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<String, dynamic>;
|
||||
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<String, dynamic>;
|
||||
// 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<String, dynamic>;
|
||||
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<String, dynamic>;
|
||||
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<String, bool>.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<String, dynamic>;
|
||||
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<String, bool>.from(jsonData['checklistItems']);
|
||||
}
|
||||
if (jsonData['remarks'] != null) {
|
||||
dataToResubmit.remarks = Map<String, String>.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<String, dynamic>;
|
||||
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<String, dynamic>;
|
||||
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<String, bool>.from(jsonData['ysiSondeChecks']);
|
||||
}
|
||||
dataToResubmit.ysiSondeComments = jsonData['ysiSondeComments'];
|
||||
|
||||
// Handle nested maps for Sensor Checks
|
||||
if (jsonData['ysiSensorChecks'] != null) {
|
||||
dataToResubmit.ysiSensorChecks = (jsonData['ysiSensorChecks'] as Map<String, dynamic>).map(
|
||||
(key, value) => MapEntry(key, Map<String, bool>.from(value)),
|
||||
);
|
||||
}
|
||||
dataToResubmit.ysiSensorComments = jsonData['ysiSensorComments'];
|
||||
|
||||
// Handle nested maps for Replacements
|
||||
if (jsonData['ysiReplacements'] != null) {
|
||||
dataToResubmit.ysiReplacements = (jsonData['ysiReplacements'] as Map<String, dynamic>).map(
|
||||
(key, value) => MapEntry(key, Map<String, String>.from(value)),
|
||||
);
|
||||
}
|
||||
|
||||
if (jsonData['vanDornChecks'] != null) {
|
||||
dataToResubmit.vanDornChecks = (jsonData['vanDornChecks'] as Map<String, dynamic>).map(
|
||||
(key, value) => MapEntry(key, Map<String, bool>.from(value)),
|
||||
);
|
||||
}
|
||||
dataToResubmit.vanDornComments = jsonData['vanDornComments'];
|
||||
dataToResubmit.vanDornCurrentSerial = jsonData['vanDornCurrentSerial'];
|
||||
dataToResubmit.vanDornNewSerial = jsonData['vanDornNewSerial'];
|
||||
|
||||
if (jsonData['vanDornReplacements'] != null) {
|
||||
dataToResubmit.vanDornReplacements = (jsonData['vanDornReplacements'] as Map<String, dynamic>).map(
|
||||
(key, value) => MapEntry(key, Map<String, String>.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<String, dynamic> result;
|
||||
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
||||
|
||||
if (method == 'POST_MULTIPART') {
|
||||
final Map<String, String> fields = Map<String, String>.from(payload['fields'] ?? {});
|
||||
// Recreate File objects from paths stored in the payload
|
||||
final Map<String, File> files = (payload['files'] as Map<String, dynamic>?)
|
||||
?.map((key, value) => MapEntry(key, File(value as String))) ?? {};
|
||||
|
||||
// Check if files still exist before attempting upload
|
||||
bool allFilesExist = true;
|
||||
List<String> 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<String, dynamic> body = Map<String, dynamic>.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: () => <String, dynamic>{});
|
||||
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: () => <String, dynamic>{}); // 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
|
||||
}
|
||||
@ -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<String> _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<void> _handleSuccessAlert(RiverInvesManualSamplingData data, List<Map<String, dynamic>>? 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
|
||||
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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<Map<String, dynamic>>? 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<Map<String, dynamic>>? 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<Map<String, dynamic>>? settings) {
|
||||
return _getChatId(settings, 'marine_investigative');
|
||||
}
|
||||
|
||||
// --- ADDED: Getter for Marine NPE Report ---
|
||||
String getMarineReportChatId(List<Map<String, dynamic>>? settings) {
|
||||
return _getChatId(settings, 'marine_report');
|
||||
}
|
||||
// ------------------------------------------
|
||||
|
||||
/// Gets the Chat ID for the River In-Situ module.
|
||||
String getRiverInSituChatId(List<Map<String, dynamic>>? 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<Map<String, dynamic>>? 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<Map<String, dynamic>>? 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<Map<String, dynamic>>? settings) {
|
||||
return _getChatId(settings, 'river_report');
|
||||
}
|
||||
// ------------------------------------------
|
||||
|
||||
/// Gets the Chat ID for the Air Manual module.
|
||||
String getAirManualChatId(List<Map<String, dynamic>>? 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<Map<String, dynamic>>? settings) {
|
||||
return _getChatId(settings, 'air_investigative');
|
||||
}
|
||||
|
||||
/// Gets the Chat ID for the Marine Investigative module from the provided settings list.
|
||||
String getMarineInvestigativeChatId(List<Map<String, dynamic>>? settings) {
|
||||
return _getChatId(settings, 'marine_investigative');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user