modfiy marine npe report to properly send api and telegram alert to server

This commit is contained in:
ALim Aidrus 2025-11-21 09:00:37 +08:00
parent cf22668576
commit d0f9d72ebd
15 changed files with 1047 additions and 870 deletions

View File

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

View File

@ -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',

View File

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

View File

@ -36,7 +36,7 @@ class SubmissionLogEntry {
final String type;
final String title;
final String stationCode;
final String senderName; // <-- ADDED
final String senderName;
final DateTime submissionDateTime;
final String? reportId;
final String status;
@ -51,7 +51,7 @@ class SubmissionLogEntry {
required this.type,
required this.title,
required this.stationCode,
required this.senderName, // <-- ADDED
required this.senderName,
required this.submissionDateTime,
this.reportId,
required this.status,
@ -97,7 +97,8 @@ class _MarineManualReportStatusLogState
// --- START: COPIED FROM SCREEN FILE ---
// This is the "single source of truth" for categories
final Map<String, List<String>> _checklistSections = {
'INTERNAL - IN-SITU SAMPLING': [ // Section title matches PDF
'INTERNAL - IN-SITU SAMPLING': [
// Section title matches PDF
'Marine manual Standard Operation Procedure (SOP)', // Item text matches PDF
'Back-up Sampling Sheet & Chain of Custody form', // Item text matches PDF
'Calibration worksheet', // Item text matches PDF
@ -125,13 +126,15 @@ class _MarineManualReportStatusLogState
'Raincoat/Poncho', // Item text matches PDF
'Ice packets', // Item text matches PDF
],
'INTERNAL-TARBALL SAMPLING': [ // Section title matches PDF
'INTERNAL-TARBALL SAMPLING': [
// Section title matches PDF
'Measuring tape (100 meter)', // Item text matches PDF
'Steel raking', // Item text matches PDF
'Aluminum foil', // Item text matches PDF
'Zipper bags', // Item text matches PDF
],
'EXTERNAL - LABORATORY': [ // Section title matches PDF
'EXTERNAL - LABORATORY': [
// Section title matches PDF
'Sufficient sets of cooler box and sampling bottles with label', // Item text matches PDF
'Field duplicate sampling bottles (if any)', // Item text matches PDF
'Blank samples sampling bottles (if any)', // Item text matches PDF
@ -152,14 +155,18 @@ class _MarineManualReportStatusLogState
void didChangeDependencies() {
super.didChangeDependencies();
// Initialize all required services from Provider
_localStorageService = Provider.of<LocalStorageService>(context, listen: false); // Added listen: false
_npeReportService = Provider.of<MarineNpeReportService>(context, listen: false); // Added listen: false
_preDepartureService =
Provider.of<MarineManualPreDepartureService>(context, listen: false); // Added listen: false
_sondeCalibrationService =
Provider.of<MarineManualSondeCalibrationService>(context, listen: false); // Added listen: false
_localStorageService =
Provider.of<LocalStorageService>(context, listen: false);
_npeReportService =
Provider.of<MarineNpeReportService>(context, listen: false);
_preDepartureService = Provider.of<MarineManualPreDepartureService>(context,
listen: false);
_sondeCalibrationService = Provider.of<MarineManualSondeCalibrationService>(
context,
listen: false);
_equipmentMaintenanceService =
Provider.of<MarineManualEquipmentMaintenanceService>(context, listen: false); // Added listen: false
Provider.of<MarineManualEquipmentMaintenanceService>(context,
listen: false);
// Load logs after services are initialized
if (_isLoading) {
@ -185,9 +192,7 @@ class _MarineManualReportStatusLogState
await _localStorageService.getAllSondeCalibrationLogs();
final equipmentMaintenanceLogs =
await _localStorageService.getAllEquipmentMaintenanceLogs();
// *** START: Fixed method name ***
final npeReportLogs = await _localStorageService.getAllNpeLogs();
// *** END: Fixed method name ***
final List<SubmissionLogEntry> tempPreSampling = [];
final List<SubmissionLogEntry> tempReport = [];
@ -199,9 +204,7 @@ class _MarineManualReportStatusLogState
type: 'Pre-Departure Checklist',
title: 'Pre-Departure Checklist',
stationCode: 'N/A',
// --- START: MODIFIED ---
senderName: (log['reporterName'] as String?) ?? 'Unknown User',
// --- END: MODIFIED ---
submissionDateTime: DateTime.tryParse(dateStr) ??
DateTime.fromMillisecondsSinceEpoch(0),
reportId: log['reportId']?.toString(),
@ -215,18 +218,12 @@ class _MarineManualReportStatusLogState
// 2. Process Sonde Calibration Logs -> Pre-Sampling
for (var log in sondeCalibrationLogs) {
// --- START: MODIFIED ---
final dateStr = log['startDateTime'] ?? '';
// --- END: MODIFIED ---
tempPreSampling.add(SubmissionLogEntry(
type: 'Sonde Calibration',
// --- START: MODIFIED LINE ---
title: 'Sonde Calibration', // Use module name as title
// --- END: MODIFIED LINE ---
title: 'Sonde Calibration',
stationCode: log['location'] ?? 'N/A',
// --- START: MODIFIED ---
senderName: (log['calibratedByUserName'] as String?) ?? 'Unknown User',
// --- END: MODIFIED ---
submissionDateTime: DateTime.tryParse(dateStr) ??
DateTime.fromMillisecondsSinceEpoch(0),
reportId: log['reportId']?.toString(),
@ -240,16 +237,12 @@ class _MarineManualReportStatusLogState
// 3. Process Equipment Maintenance Logs -> Pre-Sampling
for (var log in equipmentMaintenanceLogs) {
// --- START: MODIFIED ---
final dateStr = log['maintenanceDate'] ?? '';
// --- END: MODIFIED ---
tempPreSampling.add(SubmissionLogEntry(
type: 'Equipment Maintenance',
title: 'Equipment Maintenance',
stationCode: log['location'] ?? 'N/A',
// --- START: MODIFIED ---
senderName: (log['conductedByUserName'] as String?) ?? 'Unknown User',
// --- END: MODIFIED ---
submissionDateTime: DateTime.tryParse(dateStr) ??
DateTime.fromMillisecondsSinceEpoch(0),
reportId: log['reportId']?.toString(),
@ -277,9 +270,7 @@ class _MarineManualReportStatusLogState
type: 'NPE Report',
title: title,
stationCode: stationCode,
// --- START: MODIFIED FOR NULL SAFETY ---
senderName: (log['firstSamplerName'] as String?) ?? 'Unknown User', // <-- FIXED
// --- END: MODIFIED FOR NULL SAFETY ---
senderName: (log['firstSamplerName'] as String?) ?? 'Unknown User',
submissionDateTime: DateTime.tryParse('$dateStr $timeStr') ??
DateTime.fromMillisecondsSinceEpoch(0),
reportId: log['reportId']?.toString(),
@ -288,14 +279,15 @@ class _MarineManualReportStatusLogState
rawData: log,
serverName: log['serverConfigName'] ?? 'Unknown Server',
apiStatusRaw: log['api_status'],
ftpStatusRaw: log['ftp_status'], // NPE has FTP
ftpStatusRaw: log['ftp_status'],
));
}
// Sort logs by date (descending)
tempPreSampling
.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
tempReport.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
tempReport
.sort((a, b) => b.submissionDateTime.compareTo(a.submissionDateTime));
if (mounted) {
setState(() {
@ -328,7 +320,7 @@ class _MarineManualReportStatusLogState
log.stationCode.toLowerCase().contains(query) ||
log.serverName.toLowerCase().contains(query) ||
log.type.toLowerCase().contains(query) ||
log.senderName.toLowerCase().contains(query) || // <-- ADDED
log.senderName.toLowerCase().contains(query) ||
(log.reportId?.toLowerCase() ?? '').contains(query);
}
@ -387,23 +379,25 @@ class _MarineManualReportStatusLogState
data.location = logData['location'];
data.startDateTime = logData['startDateTime'];
data.endDateTime = logData['endDateTime'];
data.ph7Mv = (logData['ph_7_mv'] as num?)?.toDouble(); // Fixed key
data.ph7Before = (logData['ph_7_before'] as num?)?.toDouble(); // Fixed key
data.ph7After = (logData['ph_7_after'] as num?)?.toDouble(); // Fixed key
data.ph10Mv = (logData['ph_10_mv'] as num?)?.toDouble(); // Fixed key
data.ph10Before = (logData['ph_10_before'] as num?)?.toDouble(); // Fixed key
data.ph10After = (logData['ph_10_after'] as num?)?.toDouble(); // Fixed key
data.condBefore = (logData['cond_before'] as num?)?.toDouble(); // Fixed key
data.condAfter = (logData['cond_after'] as num?)?.toDouble(); // Fixed key
data.doBefore = (logData['do_before'] as num?)?.toDouble(); // Fixed key
data.doAfter = (logData['do_after'] as num?)?.toDouble(); // Fixed key
data.turbidity0Before = (logData['turbidity_0_before'] as num?)?.toDouble(); // Fixed key
data.turbidity0After = (logData['turbidity_0_after'] as num?)?.toDouble(); // Fixed key
data.ph7Mv = (logData['ph_7_mv'] as num?)?.toDouble();
data.ph7Before = (logData['ph_7_before'] as num?)?.toDouble();
data.ph7After = (logData['ph_7_after'] as num?)?.toDouble();
data.ph10Mv = (logData['ph_10_mv'] as num?)?.toDouble();
data.ph10Before = (logData['ph_10_before'] as num?)?.toDouble();
data.ph10After = (logData['ph_10_after'] as num?)?.toDouble();
data.condBefore = (logData['cond_before'] as num?)?.toDouble();
data.condAfter = (logData['cond_after'] as num?)?.toDouble();
data.doBefore = (logData['do_before'] as num?)?.toDouble();
data.doAfter = (logData['do_after'] as num?)?.toDouble();
data.turbidity0Before =
(logData['turbidity_0_before'] as num?)?.toDouble();
data.turbidity0After =
(logData['turbidity_0_after'] as num?)?.toDouble();
data.turbidity124Before =
(logData['turbidity_124_before'] as num?)?.toDouble(); // Fixed key
(logData['turbidity_124_before'] as num?)?.toDouble();
data.turbidity124After =
(logData['turbidity_124_after'] as num?)?.toDouble(); // Fixed key
data.calibrationStatus = logData['calibration_status']; // Fixed key
(logData['turbidity_124_after'] as num?)?.toDouble();
data.calibrationStatus = logData['calibration_status'];
data.remarks = logData['remarks'];
result = await _sondeCalibrationService.submitCalibration(
@ -461,9 +455,7 @@ class _MarineManualReportStatusLogState
);
}
// *** START: Fixed method name ***
result = await _equipmentMaintenanceService.submitMaintenanceReport(
// *** END: Fixed method name ***
data: data,
authProvider: authProvider,
appSettings: appSettings,
@ -483,7 +475,8 @@ class _MarineManualReportStatusLogState
data.selectedStation = logData['selectedStation'];
data.latitude = logData['latitude'];
data.longitude = logData['longitude'];
data.oxygenSaturation = (logData['oxygenSaturation'] as num?)?.toDouble();
data.oxygenSaturation =
(logData['oxygenSaturation'] as num?)?.toDouble();
data.electricalConductivity =
(logData['electricalConductivity'] as num?)?.toDouble();
data.oxygenConcentration =
@ -512,13 +505,11 @@ class _MarineManualReportStatusLogState
data.image3 = _createFileFromPath(logData['image3Path']);
data.image4 = _createFileFromPath(logData['image4Path']);
// *** START: Removed extra parameters ***
result = await _npeReportService.submitNpeReport(
data: data,
authProvider: authProvider,
logDirectory: log.rawData['logDirectory'] as String?,
);
// *** END: Removed extra parameters ***
break;
default:
@ -528,8 +519,8 @@ class _MarineManualReportStatusLogState
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
result['message'] ?? 'Resubmission process completed.')),
content:
Text(result['message'] ?? 'Resubmission process completed.')),
);
}
} catch (e) {
@ -580,9 +571,7 @@ class _MarineManualReportStatusLogState
}
/// Builds a collapsible card for a category of logs.
Widget _buildCategorySection(
String category,
List<SubmissionLogEntry> logs,
Widget _buildCategorySection(String category, List<SubmissionLogEntry> logs,
TextEditingController searchController) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 8.0),
@ -621,7 +610,8 @@ class _MarineManualReportStatusLogState
const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text('No logs match your search in this category.')))
child:
Text('No logs match your search in this category.')))
else
ListView.builder(
shrinkWrap: true,
@ -681,13 +671,11 @@ class _MarineManualReportStatusLogState
),
);
// --- START: MODIFIED SUBTITLE ---
final bool isDateValid = !log.submissionDateTime
.isAtSameMomentAs(DateTime.fromMillisecondsSinceEpoch(0));
final subtitle = isDateValid
? '${log.senderName} - ${DateFormat('yyyy-MM-dd HH:mm').format(log.submissionDateTime)}'
: '${log.senderName} - Invalid Date';
// --- END: MODIFIED SUBTITLE ---
return ExpansionTile(
key: PageStorageKey(logKey),
@ -715,11 +703,10 @@ class _MarineManualReportStatusLogState
_buildDetailRow('High-Level Status:', log.status),
_buildDetailRow('Server:', log.serverName),
_buildDetailRow('Report ID:', log.reportId ?? 'N/A'),
_buildGranularStatus('API', log.apiStatusRaw), // <-- MODIFIED
_buildGranularStatus('FTP', log.ftpStatusRaw), // <-- MODIFIED
_buildGranularStatus('API', log.apiStatusRaw),
_buildGranularStatus('FTP', log.ftpStatusRaw),
_buildDetailRow('Message:', log.message),
// --- START: ADDED BUTTONS ---
const Divider(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
@ -727,19 +714,24 @@ class _MarineManualReportStatusLogState
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton.icon(
icon: Icon(Icons.list_alt, color: Theme.of(context).colorScheme.primary),
label: Text('View Data', style: TextStyle(color: Theme.of(context).colorScheme.primary)),
icon: Icon(Icons.list_alt,
color: Theme.of(context).colorScheme.primary),
label: Text('View Data',
style: TextStyle(
color: Theme.of(context).colorScheme.primary)),
onPressed: () => _showDataDialog(context, log),
),
TextButton.icon(
icon: Icon(Icons.photo_library_outlined, color: Theme.of(context).colorScheme.secondary),
label: Text('View Images', style: TextStyle(color: Theme.of(context).colorScheme.secondary)),
icon: Icon(Icons.photo_library_outlined,
color: Theme.of(context).colorScheme.secondary),
label: Text('View Images',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary)),
onPressed: () => _showImageDialog(context, log),
),
],
),
),
// --- END: ADDED BUTTONS ---
],
),
)
@ -755,8 +747,8 @@ class _MarineManualReportStatusLogState
children: [
Expanded(
flex: 2,
child:
Text(label, style: const TextStyle(fontWeight: FontWeight.bold))),
child: Text(label,
style: const TextStyle(fontWeight: FontWeight.bold))),
const SizedBox(width: 8),
Expanded(flex: 3, child: Text(value)),
],
@ -775,9 +767,9 @@ class _MarineManualReportStatusLogState
return value.toString();
}
// --- START: ADDED WIDGET-BASED HEADER HELPER ---
/// Builds a formatted category header row for the data list.
Widget _buildCategoryHeader(BuildContext context, String title, IconData icon) {
Widget _buildCategoryHeader(
BuildContext context, String title, IconData icon) {
return Padding(
padding: const EdgeInsets.only(top: 16.0, bottom: 8.0),
child: Row(
@ -796,50 +788,55 @@ class _MarineManualReportStatusLogState
),
);
}
// --- END: ADDED WIDGET-BASED HEADER HELPER ---
// --- START: RE-INTRODUCED TABLE-BASED HELPERS ---
/// Builds a formatted category header row for the data table.
TableRow _buildCategoryRow(BuildContext context, String title, IconData icon) {
return TableRow(
/// Builds a formatted Section header (replacement for _buildCategoryRow).
Widget _buildSectionHeader(
BuildContext context, String title, IconData icon) {
return Container(
margin: const EdgeInsets.only(top: 16.0, bottom: 8.0),
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.grey.shade100,
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(4),
),
children: [
Padding(
padding: const EdgeInsets.only(top: 16.0, bottom: 8.0, left: 8.0, right: 8.0),
child: Row(
children: [
Icon(icon, size: 20, color: Theme.of(context).primaryColor),
Icon(icon, size: 18, color: Theme.of(context).primaryColor),
const SizedBox(width: 8),
Text(
Expanded(
child: Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
fontSize: 15,
color: Theme.of(context).primaryColor,
),
),
),
],
),
),
const SizedBox.shrink(), // Empty cell for the second column
],
);
}
/// Builds a formatted row for the data dialog.
TableRow _buildDataTableRow(String label, String? value, {Color? valueColor}) {
String displayValue = (value == null || value.isEmpty || value == 'null') ? 'N/A' : value;
/// Builds a responsive data row (replacement for _buildDataTableRow).
/// Uses Column for better responsiveness on small screens if needed,
/// or Row with Expanded to allow wrapping.
Widget _buildResponsiveDataRow(String label, String? value,
{Color? valueColor}) {
String displayValue =
(value == null || value.isEmpty || value == 'null') ? 'N/A' : value;
if (displayValue == '-999.0' || displayValue == '-999') {
displayValue = 'N/A';
}
return TableRow(
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
Expanded(
flex: 2,
child: Text(
label,
style: const TextStyle(
@ -848,40 +845,22 @@ class _MarineManualReportStatusLogState
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
const SizedBox(width: 8),
Expanded(
flex: 3,
child: Text(
displayValue,
style: TextStyle(fontSize: 14.0, color: valueColor),
),
),
],
);
}
/// Builds a remark row for the data dialog.
TableRow _buildRemarkTableRow(String? remark) {
if (remark == null || remark.isEmpty) {
return const TableRow(children: [SizedBox.shrink(), SizedBox.shrink()]);
}
return TableRow(
children: [
const SizedBox.shrink(), // Empty cell for the label
Padding(
padding: const EdgeInsets.only(bottom: 8.0, left: 8.0, right: 8.0),
child: Text(
"Remark: $remark",
style: TextStyle(
fontSize: 13.0,
color: Colors.grey.shade700,
fontStyle: FontStyle.italic,
fontSize: 14.0,
color: (displayValue == 'N/A') ? Colors.grey : valueColor,
),
textAlign: TextAlign.left,
),
),
],
),
);
}
// --- END: RE-INTRODUCED TABLE-BASED HELPERS ---
/// Shows the categorized and formatted data log in a dialog
void _showDataDialog(BuildContext context, SubmissionLogEntry log) {
@ -889,26 +868,21 @@ class _MarineManualReportStatusLogState
Widget dialogContent; // This will hold either a ListView or a Column
if (log.type == 'Pre-Departure Checklist') {
// --- START: Handle Pre-Departure Checklist (uses Column/ListView) ---
// --- Handle Pre-Departure Checklist (Existing Logic) ---
final items = Map<String, bool>.from(data['checklistItems'] ?? {});
final remarks = Map<String, String>.from(data['remarks'] ?? {});
// 1. Build the list of widgets
final List<Widget> contentWidgets = [];
// 2. Iterate over the DEFINED categories from the map
for (final categoryEntry in _checklistSections.entries) {
final categoryTitle = categoryEntry.key;
final categoryItems = categoryEntry.value;
// Add the category header
contentWidgets.add(_buildCategoryHeader(context, categoryTitle, Icons.check_box_outlined));
contentWidgets.add(_buildCategoryHeader(
context, categoryTitle, Icons.check_box_outlined));
// 3. Add the items for that category
contentWidgets.addAll(
categoryItems.map((itemName) {
// Find the item's status and remark from the log data
final bool value = items[itemName] ?? false; // Default to 'No'
contentWidgets.addAll(categoryItems.map((itemName) {
final bool value = items[itemName] ?? false;
final String remark = remarks[itemName] ?? '';
final String status = value ? 'Yes' : 'No';
@ -917,15 +891,13 @@ class _MarineManualReportStatusLogState
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Row 1: Item and Status
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Item Name
Expanded(
flex: 3,
child: Text(
itemName, // Use the name from the category list
itemName,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
@ -933,14 +905,15 @@ class _MarineManualReportStatusLogState
),
),
const SizedBox(width: 8),
// Status
Expanded(
flex: 1,
child: Text(
status,
style: TextStyle(
fontSize: 14.0,
color: value ? Colors.green.shade700 : Colors.red.shade700,
color: value
? Colors.green.shade700
: Colors.red.shade700,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.end,
@ -948,7 +921,6 @@ class _MarineManualReportStatusLogState
),
],
),
// Row 2: Remark (only if it exists)
if (remark.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 6.0, left: 8.0),
@ -979,27 +951,23 @@ class _MarineManualReportStatusLogState
],
),
);
}).toList()
);
// Add a divider after the category
}).toList());
contentWidgets.add(const Divider(height: 16));
}
// 4. Handle any items that were in the log but NOT in the category map
final Set<String> allCategorizedItems = _checklistSections.values.expand((list) => list).toSet();
// Handle other items
final Set<String> allCategorizedItems =
_checklistSections.values.expand((list) => list).toSet();
final List<Widget> otherItems = [];
for (final itemEntry in items.entries) {
if (!allCategorizedItems.contains(itemEntry.key)) {
// This item was not in our hard-coded map
final key = itemEntry.key;
final value = itemEntry.value;
final status = value ? 'Yes' : 'No';
final remark = remarks[key] ?? '';
otherItems.add(
Padding(
otherItems.add(Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -1007,9 +975,22 @@ class _MarineManualReportStatusLogState
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(flex: 3, child: Text(key, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14.0))),
Expanded(
flex: 3,
child: Text(key,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 14.0))),
const SizedBox(width: 8),
Expanded(flex: 1, child: Text(status, style: TextStyle(fontSize: 14.0, color: value ? Colors.green.shade700 : Colors.red.shade700, fontWeight: FontWeight.bold), textAlign: TextAlign.end)),
Expanded(
flex: 1,
child: Text(status,
style: TextStyle(
fontSize: 14.0,
color: value
? Colors.green.shade700
: Colors.red.shade700,
fontWeight: FontWeight.bold),
textAlign: TextAlign.end)),
],
),
if (remark.isNotEmpty)
@ -1018,49 +999,56 @@ class _MarineManualReportStatusLogState
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Remark: ", style: TextStyle(fontSize: 13.0, color: Colors.grey.shade700, fontStyle: FontStyle.italic)),
Expanded(child: Text(remark, style: TextStyle(fontSize: 13.0, color: Colors.grey.shade700, fontStyle: FontStyle.italic))),
Text("Remark: ",
style: TextStyle(
fontSize: 13.0,
color: Colors.grey.shade700,
fontStyle: FontStyle.italic)),
Expanded(
child: Text(remark,
style: TextStyle(
fontSize: 13.0,
color: Colors.grey.shade700,
fontStyle: FontStyle.italic))),
],
),
),
],
),
)
);
));
}
}
if (otherItems.isNotEmpty) {
contentWidgets.add(_buildCategoryHeader(context, "Other Items", Icons.help_outline));
contentWidgets.add(
_buildCategoryHeader(context, "Other Items", Icons.help_outline));
contentWidgets.addAll(otherItems);
}
if (contentWidgets.isEmpty) {
dialogContent = const Center(child: Text('No checklist items found.'));
} else {
// Build the final Column
dialogContent = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: contentWidgets,
);
}
// --- END: Handle Pre-Departure Checklist ---
} else {
// --- START: Handle ALL OTHER Log Types (uses Table) ---
final List<TableRow> tableRows = [];
// --- START: Handle ALL OTHER Log Types (NOW USING RESPONSIVE WIDGETS) ---
final List<Widget> listWidgets = [];
// --- Helper for nested maps ---
void addNestedMapRows(Map<String, dynamic> map) {
map.forEach((key, value) {
if (value is Map) {
// Handle nested maps (e.g., ysiSensorChecks)
tableRows.add(_buildDataTableRow(key, ''));
listWidgets.add(_buildResponsiveDataRow(key, ''));
value.forEach((subKey, subValue) {
tableRows.add(_buildDataTableRow(' $subKey', subValue?.toString() ?? 'N/A'));
listWidgets.add(_buildResponsiveDataRow(
' $subKey', subValue?.toString() ?? 'N/A'));
});
} else {
tableRows.add(_buildDataTableRow(key, value?.toString() ?? 'N/A'));
listWidgets.add(
_buildResponsiveDataRow(key, value?.toString() ?? 'N/A'));
}
});
}
@ -1068,133 +1056,190 @@ class _MarineManualReportStatusLogState
switch (log.type) {
case 'Sonde Calibration':
tableRows.add(_buildCategoryRow(context, 'Sonde Info', Icons.info_outline));
tableRows.add(_buildDataTableRow('Sonde Serial #', _getString(data, 'sondeSerialNumber')));
tableRows.add(_buildDataTableRow('Firmware Version', _getString(data, 'firmwareVersion')));
tableRows.add(_buildDataTableRow('KOR Version', _getString(data, 'korVersion')));
tableRows.add(_buildDataTableRow('Location', _getString(data, 'location')));
tableRows.add(_buildDataTableRow('Start Time', _getString(data, 'startDateTime')));
tableRows.add(_buildDataTableRow('End Time', _getString(data, 'endDateTime')));
tableRows.add(_buildDataTableRow('Status', _getString(data, 'calibration_status')));
tableRows.add(_buildDataTableRow('Remarks', _getString(data, 'remarks')));
listWidgets.add(
_buildSectionHeader(context, 'Sonde Info', Icons.info_outline));
listWidgets.add(_buildResponsiveDataRow(
'Sonde Serial #', _getString(data, 'sondeSerialNumber')));
listWidgets.add(_buildResponsiveDataRow(
'Firmware Version', _getString(data, 'firmwareVersion')));
listWidgets.add(_buildResponsiveDataRow(
'KOR Version', _getString(data, 'korVersion')));
listWidgets.add(
_buildResponsiveDataRow('Location', _getString(data, 'location')));
listWidgets.add(_buildResponsiveDataRow(
'Start Time', _getString(data, 'startDateTime')));
listWidgets.add(_buildResponsiveDataRow(
'End Time', _getString(data, 'endDateTime')));
listWidgets.add(_buildResponsiveDataRow(
'Status', _getString(data, 'calibration_status')));
listWidgets.add(
_buildResponsiveDataRow('Remarks', _getString(data, 'remarks')));
tableRows.add(_buildCategoryRow(context, 'pH 7.0', Icons.science_outlined));
tableRows.add(_buildDataTableRow('MV', _getString(data, 'ph_7_mv')));
tableRows.add(_buildDataTableRow('Before', _getString(data, 'ph_7_before')));
tableRows.add(_buildDataTableRow('After', _getString(data, 'ph_7_after')));
listWidgets.add(
_buildSectionHeader(context, 'pH 7.0', Icons.science_outlined));
listWidgets
.add(_buildResponsiveDataRow('MV', _getString(data, 'ph_7_mv')));
listWidgets.add(
_buildResponsiveDataRow('Before', _getString(data, 'ph_7_before')));
listWidgets.add(
_buildResponsiveDataRow('After', _getString(data, 'ph_7_after')));
tableRows.add(_buildCategoryRow(context, 'pH 10.0', Icons.science_outlined));
tableRows.add(_buildDataTableRow('MV', _getString(data, 'ph_10_mv')));
tableRows.add(_buildDataTableRow('Before', _getString(data, 'ph_10_before')));
tableRows.add(_buildDataTableRow('After', _getString(data, 'ph_10_after')));
listWidgets.add(
_buildSectionHeader(context, 'pH 10.0', Icons.science_outlined));
listWidgets.add(
_buildResponsiveDataRow('MV', _getString(data, 'ph_10_mv')));
listWidgets.add(_buildResponsiveDataRow(
'Before', _getString(data, 'ph_10_before')));
listWidgets.add(
_buildResponsiveDataRow('After', _getString(data, 'ph_10_after')));
tableRows.add(_buildCategoryRow(context, 'Conductivity', Icons.thermostat));
tableRows.add(_buildDataTableRow('Before', _getString(data, 'cond_before')));
tableRows.add(_buildDataTableRow('After', _getString(data, 'cond_after')));
listWidgets.add(
_buildSectionHeader(context, 'Conductivity', Icons.thermostat));
listWidgets.add(_buildResponsiveDataRow(
'Before', _getString(data, 'cond_before')));
listWidgets.add(
_buildResponsiveDataRow('After', _getString(data, 'cond_after')));
tableRows.add(_buildCategoryRow(context, 'Dissolved Oxygen', Icons.air));
tableRows.add(_buildDataTableRow('Before', _getString(data, 'do_before')));
tableRows.add(_buildDataTableRow('After', _getString(data, 'do_after')));
listWidgets.add(
_buildSectionHeader(context, 'Dissolved Oxygen', Icons.air));
listWidgets.add(
_buildResponsiveDataRow('Before', _getString(data, 'do_before')));
listWidgets.add(
_buildResponsiveDataRow('After', _getString(data, 'do_after')));
tableRows.add(_buildCategoryRow(context, 'Turbidity', Icons.waves));
tableRows.add(_buildDataTableRow('0 NTU Before', _getString(data, 'turbidity_0_before')));
tableRows.add(_buildDataTableRow('0 NTU After', _getString(data, 'turbidity_0_after')));
tableRows.add(_buildDataTableRow('124 NTU Before', _getString(data, 'turbidity_124_before')));
tableRows.add(_buildDataTableRow('124 NTU After', _getString(data, 'turbidity_124_after')));
listWidgets.add(
_buildSectionHeader(context, 'Turbidity', Icons.waves));
listWidgets.add(_buildResponsiveDataRow(
'0 NTU Before', _getString(data, 'turbidity_0_before')));
listWidgets.add(_buildResponsiveDataRow(
'0 NTU After', _getString(data, 'turbidity_0_after')));
listWidgets.add(_buildResponsiveDataRow(
'124 NTU Before', _getString(data, 'turbidity_124_before')));
listWidgets.add(_buildResponsiveDataRow(
'124 NTU After', _getString(data, 'turbidity_124_after')));
break;
case 'Equipment Maintenance':
tableRows.add(_buildCategoryRow(context, 'YSI Sonde Checks', Icons.build_circle_outlined));
listWidgets.add(_buildSectionHeader(
context, 'YSI Sonde Checks', Icons.build_circle_outlined));
if (data['ysiSondeChecks'] != null) {
addNestedMapRows(Map<String, dynamic>.from(data['ysiSondeChecks']));
}
tableRows.add(_buildDataTableRow('Comments', _getString(data, 'ysiSondeComments')));
listWidgets.add(_buildResponsiveDataRow(
'Comments', _getString(data, 'ysiSondeComments')));
tableRows.add(_buildCategoryRow(context, 'YSI Sensor Checks', Icons.sensors));
listWidgets.add(_buildSectionHeader(
context, 'YSI Sensor Checks', Icons.sensors));
if (data['ysiSensorChecks'] != null) {
addNestedMapRows(Map<String, dynamic>.from(data['ysiSensorChecks']));
addNestedMapRows(
Map<String, dynamic>.from(data['ysiSensorChecks']));
}
tableRows.add(_buildDataTableRow('Comments', _getString(data, 'ysiSensorComments')));
listWidgets.add(_buildResponsiveDataRow(
'Comments', _getString(data, 'ysiSensorComments')));
tableRows.add(_buildCategoryRow(context, 'YSI Replacements', Icons.published_with_changes));
listWidgets.add(_buildSectionHeader(
context, 'YSI Replacements', Icons.published_with_changes));
if (data['ysiReplacements'] != null) {
addNestedMapRows(Map<String, dynamic>.from(data['ysiReplacements']));
addNestedMapRows(
Map<String, dynamic>.from(data['ysiReplacements']));
}
tableRows.add(_buildCategoryRow(context, 'Van Dorn Checks', Icons.opacity));
listWidgets.add(
_buildSectionHeader(context, 'Van Dorn Checks', Icons.opacity));
if (data['vanDornChecks'] != null) {
addNestedMapRows(Map<String, dynamic>.from(data['vanDornChecks']));
}
tableRows.add(_buildDataTableRow('Comments', _getString(data, 'vanDornComments')));
tableRows.add(_buildDataTableRow('Current Serial', _getString(data, 'vanDornCurrentSerial')));
tableRows.add(_buildDataTableRow('New Serial', _getString(data, 'vanDornNewSerial')));
listWidgets.add(_buildResponsiveDataRow(
'Comments', _getString(data, 'vanDornComments')));
listWidgets.add(_buildResponsiveDataRow(
'Current Serial', _getString(data, 'vanDornCurrentSerial')));
listWidgets.add(_buildResponsiveDataRow(
'New Serial', _getString(data, 'vanDornNewSerial')));
tableRows.add(_buildCategoryRow(context, 'Van Dorn Replacements', Icons.published_with_changes));
listWidgets.add(_buildSectionHeader(context, 'Van Dorn Replacements',
Icons.published_with_changes));
if (data['vanDornReplacements'] != null) {
addNestedMapRows(Map<String, dynamic>.from(data['vanDornReplacements']));
addNestedMapRows(
Map<String, dynamic>.from(data['vanDornReplacements']));
}
break;
case 'NPE Report':
tableRows.add(_buildCategoryRow(context, 'Event Info', Icons.calendar_today));
tableRows.add(_buildDataTableRow('Date', _getString(data, 'eventDate')));
tableRows.add(_buildDataTableRow('Time', _getString(data, 'eventTime')));
tableRows.add(_buildDataTableRow('Sampler', _getString(data, 'firstSamplerName')));
listWidgets.add(
_buildSectionHeader(context, 'Event Info', Icons.calendar_today));
listWidgets.add(
_buildResponsiveDataRow('Date', _getString(data, 'eventDate')));
listWidgets.add(
_buildResponsiveDataRow('Time', _getString(data, 'eventTime')));
listWidgets.add(_buildResponsiveDataRow(
'Sampler', _getString(data, 'firstSamplerName')));
tableRows.add(_buildCategoryRow(context, 'Location', Icons.location_on_outlined));
listWidgets.add(_buildSectionHeader(
context, 'Location', Icons.location_on_outlined));
if (data['selectedStation'] != null) {
tableRows.add(_buildDataTableRow('Station', _getString(data['selectedStation'], 'man_station_name') ?? _getString(data['selectedStation'], 'tbl_station_name')));
listWidgets.add(_buildResponsiveDataRow(
'Station',
_getString(data['selectedStation'], 'man_station_name') ??
_getString(data['selectedStation'], 'tbl_station_name')));
} else {
tableRows.add(_buildDataTableRow('Location', _getString(data, 'locationDescription')));
tableRows.add(_buildDataTableRow('State', _getString(data, 'stateName')));
listWidgets.add(_buildResponsiveDataRow(
'Location', _getString(data, 'locationDescription')));
listWidgets.add(
_buildResponsiveDataRow('State', _getString(data, 'stateName')));
}
tableRows.add(_buildDataTableRow('Latitude', _getString(data, 'latitude')));
tableRows.add(_buildDataTableRow('Longitude', _getString(data, 'longitude')));
listWidgets.add(
_buildResponsiveDataRow('Latitude', _getString(data, 'latitude')));
listWidgets.add(_buildResponsiveDataRow(
'Longitude', _getString(data, 'longitude')));
tableRows.add(_buildCategoryRow(context, 'Parameters', Icons.bar_chart));
tableRows.add(_buildDataTableRow('Oxygen Conc (mg/L)', _getString(data, 'oxygenConcentration')));
tableRows.add(_buildDataTableRow('Oxygen Sat (%)', _getString(data, 'oxygenSaturation')));
tableRows.add(_buildDataTableRow('pH', _getString(data, 'ph')));
tableRows.add(_buildDataTableRow('Conductivity (µS/cm)', _getString(data, 'electricalConductivity')));
tableRows.add(_buildDataTableRow('Temperature (°C)', _getString(data, 'temperature')));
tableRows.add(_buildDataTableRow('Turbidity (NTU)', _getString(data, 'turbidity')));
listWidgets.add(
_buildSectionHeader(context, 'Parameters', Icons.bar_chart));
listWidgets.add(_buildResponsiveDataRow(
'Oxygen Conc (mg/L)', _getString(data, 'oxygenConcentration')));
listWidgets.add(_buildResponsiveDataRow(
'Oxygen Sat (%)', _getString(data, 'oxygenSaturation')));
listWidgets.add(_buildResponsiveDataRow('pH', _getString(data, 'ph')));
listWidgets.add(_buildResponsiveDataRow('Conductivity (µS/cm)',
_getString(data, 'electricalConductivity')));
listWidgets.add(_buildResponsiveDataRow(
'Temperature (°C)', _getString(data, 'temperature')));
listWidgets.add(_buildResponsiveDataRow(
'Turbidity (NTU)', _getString(data, 'turbidity')));
tableRows.add(_buildCategoryRow(context, 'Observations', Icons.warning_amber_rounded));
listWidgets.add(_buildSectionHeader(
context, 'Observations', Icons.warning_amber_rounded));
if (data['fieldObservations'] != null) {
final observations = Map<String, bool>.from(data['fieldObservations']);
final observations =
Map<String, bool>.from(data['fieldObservations']);
observations.forEach((key, value) {
if(value) tableRows.add(_buildDataTableRow(key, 'Checked'));
if (value) listWidgets.add(_buildResponsiveDataRow(key, 'Checked'));
});
}
tableRows.add(_buildDataTableRow('Other Remarks', _getString(data, 'othersObservationRemark')));
tableRows.add(_buildDataTableRow('Possible Source', _getString(data, 'possibleSource')));
listWidgets.add(_buildResponsiveDataRow(
'Other Remarks', _getString(data, 'othersObservationRemark')));
listWidgets.add(_buildResponsiveDataRow(
'Possible Source', _getString(data, 'possibleSource')));
if (data['selectedTarballClassification'] != null) {
tableRows.add(_buildDataTableRow('Tarball Class', _getString(data['selectedTarballClassification'], 'classification_name')));
listWidgets.add(_buildResponsiveDataRow(
'Tarball Class',
_getString(data['selectedTarballClassification'],
'classification_name')));
}
break;
default:
tableRows.add(_buildDataTableRow('Error', 'No data view configured for log type: ${log.type}'));
listWidgets.add(_buildResponsiveDataRow(
'Error', 'No data view configured for log type: ${log.type}'));
}
// Assign the Table as the content for the 'else' block
dialogContent = Table(
columnWidths: const {
0: IntrinsicColumnWidth(), // Label column
1: FlexColumnWidth(), // Value column
},
border: TableBorder(
horizontalInside: BorderSide(
color: Colors.grey.shade300,
width: 0.5,
),
),
children: tableRows,
// Assign the content
dialogContent = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: listWidgets,
);
// --- END: Handle ALL OTHER Log Types ---
}
// Now, dialogContent is guaranteed to be assigned
showDialog(
context: context,
builder: (context) {
@ -1203,7 +1248,7 @@ class _MarineManualReportStatusLogState
content: SizedBox(
width: double.maxFinite,
child: SingleChildScrollView(
child: dialogContent, // <-- This is now safe
child: dialogContent,
),
),
actions: [
@ -1218,7 +1263,6 @@ class _MarineManualReportStatusLogState
}
// --- END: MODIFIED METHOD ---
/// Shows the image gallery dialog
void _showImageDialog(BuildContext context, SubmissionLogEntry log) {
final List<ImageLogEntry> imageEntries = [];
@ -1261,7 +1305,8 @@ class _MarineManualReportStatusLogState
),
itemBuilder: (context, index) {
final imageEntry = imageEntries[index];
final bool hasRemark = imageEntry.remark != null && imageEntry.remark!.isNotEmpty;
final bool hasRemark =
imageEntry.remark != null && imageEntry.remark!.isNotEmpty;
return Card(
clipBehavior: Clip.antiAlias,
@ -1329,7 +1374,8 @@ class _MarineManualReportStatusLogState
}
/// Helper for _showImageDialog
void _addImagesToList(SubmissionLogEntry log, Map<String, String?> imageRemarkMap, List<ImageLogEntry> imageEntries) {
void _addImagesToList(SubmissionLogEntry log,
Map<String, String?> imageRemarkMap, List<ImageLogEntry> imageEntries) {
for (final entry in imageRemarkMap.entries) {
final imageKey = entry.key;
final remarkKey = entry.value;
@ -1338,7 +1384,8 @@ class _MarineManualReportStatusLogState
if (path != null && path is String && path.isNotEmpty) {
final file = File(path);
if (file.existsSync()) {
final remark = (remarkKey != null ? log.rawData[remarkKey] as String? : null);
final remark =
(remarkKey != null ? log.rawData[remarkKey] as String? : null);
imageEntries.add(ImageLogEntry(file: file, remark: remark));
}
}
@ -1374,17 +1421,21 @@ class _MarineManualReportStatusLogState
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$type Status:', style: const TextStyle(fontWeight: FontWeight.bold)),
Text('$type Status:',
style: const TextStyle(fontWeight: FontWeight.bold)),
...statuses.map((s) {
final serverName = s['server_name'] ?? s['config_name'] ?? 'Server N/A';
final serverName =
s['server_name'] ?? s['config_name'] ?? 'Server N/A';
final status = s['message'] ?? 'N/A';
final bool isSuccess = s['success'] as bool? ?? false;
final IconData icon = isSuccess ? Icons.check_circle_outline : Icons.error_outline;
final IconData icon =
isSuccess ? Icons.check_circle_outline : Icons.error_outline;
final Color color = isSuccess ? Colors.green : Colors.red;
String detailLabel = (s['type'] != null) ? '(${s['type']})' : '';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 8.0),
padding:
const EdgeInsets.symmetric(vertical: 3.0, horizontal: 8.0),
child: Row(
children: [
Icon(icon, size: 16, color: color),

View File

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

View File

@ -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] : '';

View File

@ -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] : '';

View File

@ -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(
// Find limits for this specific station
final limitData = marineLimits.firstWhere(
(l) =>
l['param_parameter_list'] == limitName &&
l['station_id']?.toString() == stationId.toString(),
orElse: () => {},
);
// --- END FIX ---
}
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,

View File

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

View File

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

View File

@ -1,4 +1,4 @@
// lib/services/marine_npe_report_service.dart
// lib/services/marine_tarball_sampling_service.dart
import 'dart:async';
import 'dart:io';

View File

@ -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 ---
} 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;
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;
// *** 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()) {
debugPrint("Retry failed for API task $taskId: File ${file.path} (key: ${entry.key}) no longer 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 Map<String, dynamic> body = Map<String, dynamic>.from(payload['body'] ?? {});
result = await _baseApiService.post(baseUrl, endpoint, body);
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();
if (method == 'POST_MULTIPART') {
final Map<String, String> fields = Map<String, String>.from(payload['fields'] ?? {});
final Map<String, File> files = (payload['files'] as Map<String, dynamic>?)
?.map((key, value) => MapEntry(key, File(value as String))) ?? {};
bool allFilesExist = true;
for (var entry in files.entries) {
if (!await entry.value.exists()) {
allFilesExist = false;
}
}
if (!allFilesExist) {
await _dbHelper.deleteRequestFromQueue(taskId);
return false;
}
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'] ?? {});
final result = await _baseApiService.post(baseUrl, endpoint, body);
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
}

View File

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

View File

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

View File

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