diff --git a/lib/auth_provider.dart b/lib/auth_provider.dart index c2c4c70..14c8ee9 100644 --- a/lib/auth_provider.dart +++ b/lib/auth_provider.dart @@ -39,8 +39,9 @@ class AuthProvider with ChangeNotifier { List>? _positions; List>? _airClients; List>? _airManualStations; - // --- ADDED FOR STATE LIST --- List>? _states; + List>? _appSettings; + List>? _parameterLimits; // --- Getters for UI access --- List>? get allUsers => _allUsers; @@ -54,10 +55,11 @@ class AuthProvider with ChangeNotifier { List>? get positions => _positions; List>? get airClients => _airClients; List>? get airManualStations => _airManualStations; - // --- ADDED FOR STATE LIST --- List>? get states => _states; + List>? get appSettings => _appSettings; + List>? get parameterLimits => _parameterLimits; - // --- SharedPreferences Keys (made public for BaseApiService) --- + // --- SharedPreferences Keys --- static const String tokenKey = 'jwt_token'; static const String userEmailKey = 'user_email'; static const String profileDataKey = 'user_profile_data'; @@ -78,9 +80,11 @@ class AuthProvider with ChangeNotifier { _jwtToken = prefs.getString(tokenKey); _userEmail = prefs.getString(userEmailKey); _isFirstLogin = prefs.getBool(isFirstLoginKey) ?? true; - final lastSyncMillis = prefs.getInt(lastSyncTimestampKey); - if (lastSyncMillis != null) { - _lastSyncTimestamp = DateTime.fromMillisecondsSinceEpoch(lastSyncMillis); + + // MODIFIED: Switched to getting a string for the ISO8601 timestamp + final lastSyncString = prefs.getString(lastSyncTimestampKey); + if (lastSyncString != null) { + _lastSyncTimestamp = DateTime.parse(lastSyncString); } // Always load from local DB first for instant startup @@ -98,58 +102,53 @@ class AuthProvider with ChangeNotifier { notifyListeners(); } - /// The main function to sync all app data. It checks for an internet connection - /// and fetches from the server if available, otherwise it relies on the local cache. + /// The main function to sync all app data using the delta-sync strategy. Future syncAllData({bool forceRefresh = false}) async { final connectivityResult = await Connectivity().checkConnectivity(); - if (connectivityResult != ConnectivityResult.none) { - debugPrint("AuthProvider: Device is ONLINE. Fetching fresh data from server."); - await _fetchDataFromServer(); - } else { - debugPrint("AuthProvider: Device is OFFLINE. Data is already loaded from cache."); + if (connectivityResult == ConnectivityResult.none) { + debugPrint("AuthProvider: Device is OFFLINE. Skipping sync."); + return; + } + + debugPrint("AuthProvider: Device is ONLINE. Starting delta sync."); + final prefs = await SharedPreferences.getInstance(); + + // If 'forceRefresh' is true, sync all data by passing a null timestamp. + final String? lastSync = forceRefresh ? null : prefs.getString(lastSyncTimestampKey); + + // Record the time BEFORE the sync starts. This will be our new timestamp on success. + // Use UTC for consistency across timezones. + final newSyncTimestamp = DateTime.now().toUtc().toIso8601String(); + + final result = await _apiService.syncAllData(lastSyncTimestamp: lastSync); + + if (result['success']) { + debugPrint("AuthProvider: Delta sync successful. Updating last sync timestamp."); + // On success, save the new timestamp for the next run. + await prefs.setString(lastSyncTimestampKey, newSyncTimestamp); + _lastSyncTimestamp = DateTime.parse(newSyncTimestamp); + + // After updating the DB, reload data from the cache into memory to update the UI. + await _loadDataFromCache(); + notifyListeners(); + } else { + debugPrint("AuthProvider: Delta sync failed. Timestamp not updated."); } - notifyListeners(); } /// A dedicated method to refresh only the profile. Future refreshProfile() async { final result = await _apiService.refreshProfile(); if (result['success']) { - // Update the profile data in the provider state _profileData = result['data']; - // Persist the updated profile data in SharedPreferences + // Persist the updated profile data + await _dbHelper.saveProfile(_profileData!); final prefs = await SharedPreferences.getInstance(); await prefs.setString(profileDataKey, jsonEncode(_profileData)); notifyListeners(); } } - /// Fetches all master data from the server and caches it locally. - Future _fetchDataFromServer() async { - final result = await _apiService.syncAllData(); - if (result['success']) { - final data = result['data']; - _profileData = data['profile']; - _allUsers = data['allUsers'] != null ? List>.from(data['allUsers']) : null; - _tarballStations = data['tarballStations'] != null ? List>.from(data['tarballStations']) : null; - _manualStations = data['manualStations'] != null ? List>.from(data['manualStations']) : null; - _tarballClassifications = data['tarballClassifications'] != null ? List>.from(data['tarballClassifications']) : null; - _riverManualStations = data['riverManualStations'] != null ? List>.from(data['riverManualStations']) : null; - _riverTriennialStations = data['riverTriennialStations'] != null ? List>.from(data['riverTriennialStations']) : null; - _departments = data['departments'] != null ? List>.from(data['departments']) : null; - _companies = data['companies'] != null ? List>.from(data['companies']) : null; - _positions = data['positions'] != null ? List>.from(data['positions']) : null; - _airClients = data['airClients'] != null ? List>.from(data['airClients']) : null; - _airManualStations = data['airManualStations'] != null ? List>.from(data['airManualStations']) : null; - - // --- ADDED FOR STATE LIST --- - // Note: `syncAllData` in ApiService must be updated to fetch 'states' - _states = data['states'] != null ? List>.from(data['states']) : null; - - await setLastSyncTimestamp(DateTime.now()); - } - } - /// Loads all master data from the local cache using DatabaseHelper. Future _loadDataFromCache() async { _profileData = await _dbHelper.loadProfile(); @@ -164,11 +163,10 @@ class AuthProvider with ChangeNotifier { _positions = await _dbHelper.loadPositions(); _airClients = await _dbHelper.loadAirClients(); _airManualStations = await _dbHelper.loadAirManualStations(); - - // --- ADDED FOR STATE LIST --- - // Note: `loadStates()` must be added to your DatabaseHelper class _states = await _dbHelper.loadStates(); - + // ADDED: Load new data types from the local database + _appSettings = await _dbHelper.loadAppSettings(); + _parameterLimits = await _dbHelper.loadParameterLimits(); debugPrint("AuthProvider: All master data loaded from local DB cache."); } @@ -187,6 +185,7 @@ class AuthProvider with ChangeNotifier { await _dbHelper.saveProfile(profile); debugPrint('AuthProvider: Login successful. Session and profile persisted.'); + // Perform a full refresh on login to ensure data is pristine. await syncAllData(forceRefresh: true); } @@ -198,13 +197,6 @@ class AuthProvider with ChangeNotifier { notifyListeners(); } - Future setLastSyncTimestamp(DateTime timestamp) async { - _lastSyncTimestamp = timestamp; - final prefs = await SharedPreferences.getInstance(); - await prefs.setInt(lastSyncTimestampKey, timestamp.millisecondsSinceEpoch); - notifyListeners(); - } - Future setIsFirstLogin(bool value) async { _isFirstLogin = value; final prefs = await SharedPreferences.getInstance(); @@ -230,12 +222,17 @@ class AuthProvider with ChangeNotifier { _positions = null; _airClients = null; _airManualStations = null; - - // --- ADDED FOR STATE LIST --- _states = null; + // ADDED: Clear new data on logout + _appSettings = null; + _parameterLimits = null; final prefs = await SharedPreferences.getInstance(); - await prefs.clear(); + // MODIFIED: Removed keys individually for safer logout + await prefs.remove(tokenKey); + await prefs.remove(userEmailKey); + await prefs.remove(profileDataKey); + await prefs.remove(lastSyncTimestampKey); await prefs.setBool(isFirstLoginKey, true); debugPrint('AuthProvider: All session and cached data cleared.'); diff --git a/lib/screens/air/manual/air_manual_collection_screen.dart b/lib/screens/air/manual/air_manual_collection_screen.dart index 935bbb7..5e08b27 100644 --- a/lib/screens/air/manual/air_manual_collection_screen.dart +++ b/lib/screens/air/manual/air_manual_collection_screen.dart @@ -33,6 +33,7 @@ class _AirManualCollectionScreenState extends State { .getPendingInstallations(); } + // MODIFIED: This method now fetches appSettings and passes it to the service. Future _submitCollection() async { if (_selectedInstallation == null) { ScaffoldMessenger.of(context).showSnackBar( @@ -42,9 +43,13 @@ class _AirManualCollectionScreenState extends State { } setState(() => _isLoading = true); + // Get the required services and providers. final service = Provider.of(context, listen: false); - // MODIFIED: Pass the selected installation data to the service for the Telegram alert. - final result = await service.submitCollection(_collectionData, _selectedInstallation!); + final authProvider = Provider.of(context, listen: false); + final appSettings = authProvider.appSettings; + + // Pass the selected installation data and appSettings to the service. + final result = await service.submitCollection(_collectionData, _selectedInstallation!, appSettings); setState(() => _isLoading = false); @@ -152,4 +157,4 @@ class _AirManualCollectionScreenState extends State { ), ); } -} +} \ No newline at end of file diff --git a/lib/screens/air/manual/air_manual_installation_screen.dart b/lib/screens/air/manual/air_manual_installation_screen.dart index e78dc4c..320320e 100644 --- a/lib/screens/air/manual/air_manual_installation_screen.dart +++ b/lib/screens/air/manual/air_manual_installation_screen.dart @@ -47,20 +47,26 @@ class _AirManualInstallationScreenState length, (_) => chars.codeUnitAt(rnd.nextInt(chars.length)))); } + // MODIFIED: This method now fetches appSettings and passes it to the service. Future _submitInstallation() async { setState(() => _isLoading = true); + // Get the required services and providers. final service = Provider.of(context, listen: false); - final result = await service.submitInstallation(_installationData); + final authProvider = Provider.of(context, listen: false); + final appSettings = authProvider.appSettings; + + // Pass the appSettings list to the submit method. + final result = await service.submitInstallation(_installationData, appSettings); setState(() => _isLoading = false); if (!mounted) return; final message = result['message'] ?? 'An unknown error occurred.'; - final color = (result['status'] == 'L1' || result['status'] == 'S1') + final color = (result['status'] == 'S1' || result['status'] == 'S2') ? Colors.green - : Colors.red; + : (result['status'] == 'L2_PENDING_IMAGES' ? Colors.orange : Colors.red); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message), backgroundColor: color), @@ -82,4 +88,4 @@ class _AirManualInstallationScreenState ), ); } -} +} \ No newline at end of file diff --git a/lib/screens/marine/manual/data_status_log.dart b/lib/screens/marine/manual/data_status_log.dart index 0243b3e..9bde34a 100644 --- a/lib/screens/marine/manual/data_status_log.dart +++ b/lib/screens/marine/manual/data_status_log.dart @@ -1,6 +1,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; // Added for accessing AuthProvider +import 'package:environment_monitoring_app/auth_provider.dart'; // Added for AuthProvider type import 'package:environment_monitoring_app/models/tarball_data.dart'; import 'package:environment_monitoring_app/models/in_situ_sampling_data.dart'; import 'package:environment_monitoring_app/services/local_storage_service.dart'; @@ -140,6 +142,7 @@ class _MarineManualDataStatusLogState extends State { (log.reportId?.toLowerCase() ?? '').contains(query); } + // MODIFIED: This method now fetches appSettings from AuthProvider before resubmitting. Future _resubmitData(SubmissionLogEntry log) async { final logKey = log.reportId ?? log.submissionDateTime.toIso8601String(); if (mounted) { @@ -149,7 +152,11 @@ class _MarineManualDataStatusLogState extends State { } try { - final result = await _performResubmission(log); + // Get the appSettings from the AuthProvider to pass to the API service. + final authProvider = Provider.of(context, listen: false); + final appSettings = authProvider.appSettings; + + final result = await _performResubmission(log, appSettings); final logData = log.rawData; logData['submissionStatus'] = result['status']; @@ -183,7 +190,8 @@ class _MarineManualDataStatusLogState extends State { } } - Future> _performResubmission(SubmissionLogEntry log) async { + // MODIFIED: This method now requires appSettings to pass to the API service. + Future> _performResubmission(SubmissionLogEntry log, List>? appSettings) async { final logData = log.rawData; if (log.type == 'Manual Sampling') { @@ -236,7 +244,8 @@ class _MarineManualDataStatusLogState extends State { return _marineApiService.submitInSituSample( formData: dataToResubmit.toApiFormData(), imageFiles: imageFiles, - inSituData: dataToResubmit, // Added this required parameter + inSituData: dataToResubmit, + appSettings: appSettings, // Added this required parameter ); } else if (log.type == 'Tarball Sampling') { final int? firstSamplerId = int.tryParse(logData['first_sampler_user_id']?.toString() ?? ''); @@ -267,7 +276,11 @@ class _MarineManualDataStatusLogState extends State { } } - return _marineApiService.submitTarballSample(formData: dataToResubmit.toFormData(), imageFiles: imageFiles); + return _marineApiService.submitTarballSample( + formData: dataToResubmit.toFormData(), + imageFiles: imageFiles, + appSettings: appSettings, // Added this required parameter + ); } throw Exception('Unknown submission type: ${log.type}'); diff --git a/lib/screens/marine/manual/in_situ_sampling.dart b/lib/screens/marine/manual/in_situ_sampling.dart index 9eb8074..bdce32f 100644 --- a/lib/screens/marine/manual/in_situ_sampling.dart +++ b/lib/screens/marine/manual/in_situ_sampling.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; // ADDED: Import for date formatting +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import 'package:environment_monitoring_app/auth_provider.dart'; // ADDED: Import for AuthProvider import '../../../models/in_situ_sampling_data.dart'; import '../../../services/in_situ_sampling_service.dart'; @@ -79,8 +80,12 @@ class _MarineInSituSamplingState extends State { Future _submitForm() async { setState(() => _isLoading = true); - // Use the service to submit the data. - final result = await _samplingService.submitData(_data); + // MODIFIED: Get the appSettings list from AuthProvider. + final authProvider = Provider.of(context, listen: false); + final appSettings = authProvider.appSettings; + + // MODIFIED: Pass the appSettings to the submitData method. + final result = await _samplingService.submitData(_data, appSettings); if (!mounted) return; diff --git a/lib/screens/marine/manual/tarball_sampling.dart b/lib/screens/marine/manual/tarball_sampling.dart index 39c83c8..b2ae23f 100644 --- a/lib/screens/marine/manual/tarball_sampling.dart +++ b/lib/screens/marine/manual/tarball_sampling.dart @@ -86,12 +86,19 @@ class _MarineTarballSamplingState extends State { Future _getCurrentLocation() async { /* ... Location logic ... */ } void _calculateDistance() { /* ... Distance logic ... */ } + // MODIFIED: This method now fetches appSettings and passes it to the API service. Future _submitForm() async { setState(() => _isLoading = true); + // Get the appSettings list from AuthProvider. + final authProvider = Provider.of(context, listen: false); + final appSettings = authProvider.appSettings; + + // Pass the appSettings list to the submit method. final result = await _marineApiService.submitTarballSample( formData: _data.toFormData(), imageFiles: _data.toImageFiles(), + appSettings: appSettings, ); if (!mounted) return; @@ -153,4 +160,4 @@ class _MarineTarballSamplingState extends State { Widget _buildForm1() { /* ... UI for Step 1 ... */ return Container(); } Widget _buildForm2() { /* ... UI for Step 2 ... */ return Container(); } Widget _buildForm3() { /* ... UI for Step 3 ... */ return Container(); } -} +} \ No newline at end of file diff --git a/lib/screens/marine/manual/tarball_sampling_step3_summary.dart b/lib/screens/marine/manual/tarball_sampling_step3_summary.dart index a69d454..b73678c 100644 --- a/lib/screens/marine/manual/tarball_sampling_step3_summary.dart +++ b/lib/screens/marine/manual/tarball_sampling_step3_summary.dart @@ -20,13 +20,20 @@ class _TarballSamplingStep3SummaryState extends State _submitForm() async { setState(() => _isLoading = true); + // Get the appSettings list from AuthProvider. + final authProvider = Provider.of(context, listen: false); + final appSettings = authProvider.appSettings; + // Step 1: Orchestrated Server Submission + // Pass the appSettings list to the submit method. final result = await _marineApiService.submitTarballSample( formData: widget.data.toFormData(), imageFiles: widget.data.toImageFiles(), + appSettings: appSettings, ); if (!mounted) return; diff --git a/lib/screens/river/manual/data_status_log.dart b/lib/screens/river/manual/data_status_log.dart index 34af0eb..cb046b2 100644 --- a/lib/screens/river/manual/data_status_log.dart +++ b/lib/screens/river/manual/data_status_log.dart @@ -1,6 +1,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; // ADDED: Import for Provider +import 'package:environment_monitoring_app/auth_provider.dart'; // ADDED: Import for AuthProvider import '../../../../models/river_in_situ_sampling_data.dart'; import '../../../../services/local_storage_service.dart'; @@ -142,6 +144,7 @@ class _RiverDataStatusLogState extends State { (log.reportId?.toLowerCase() ?? '').contains(query); } + // MODIFIED: This method now fetches appSettings from AuthProvider before resubmitting. Future _resubmitData(SubmissionLogEntry log) async { final logKey = log.reportId ?? log.submissionDateTime.toIso8601String(); if (mounted) { @@ -151,6 +154,10 @@ class _RiverDataStatusLogState extends State { } try { + // Get the appSettings from the AuthProvider to pass to the API service. + final authProvider = Provider.of(context, listen: false); + final appSettings = authProvider.appSettings; + final logData = log.rawData; final dataToResubmit = RiverInSituSamplingData.fromJson(logData); final Map imageFiles = {}; @@ -166,9 +173,11 @@ class _RiverDataStatusLogState extends State { } } + // Pass the appSettings list to the submit method. final result = await _riverApiService.submitInSituSample( formData: dataToResubmit.toApiFormData(), imageFiles: imageFiles, + appSettings: appSettings, ); logData['submissionStatus'] = result['status']; diff --git a/lib/screens/river/manual/in_situ_sampling.dart b/lib/screens/river/manual/in_situ_sampling.dart index ca10051..b86bb2d 100644 --- a/lib/screens/river/manual/in_situ_sampling.dart +++ b/lib/screens/river/manual/in_situ_sampling.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import 'package:environment_monitoring_app/auth_provider.dart'; // ADDED: Import for AuthProvider import '../../../models/river_in_situ_sampling_data.dart'; import '../../../services/river_in_situ_sampling_service.dart'; @@ -66,10 +67,16 @@ class _RiverInSituSamplingScreenState extends State { } } + // MODIFIED: This method now fetches appSettings and passes it to the service. Future _submitForm() async { setState(() => _isLoading = true); - final result = await _samplingService.submitData(_data); + // Get the appSettings list from AuthProvider. + final authProvider = Provider.of(context, listen: false); + final appSettings = authProvider.appSettings; + + // Pass the appSettings list to the submitData method. + final result = await _samplingService.submitData(_data, appSettings); if (!mounted) return; diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 335fe33..30c95d6 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -12,18 +12,15 @@ class SettingsScreen extends StatefulWidget { } class _SettingsScreenState extends State { + // SettingsService is now a utility, it doesn't hold state. final SettingsService _settingsService = SettingsService(); bool _isSyncingData = false; - bool _isSyncingSettings = false; - String _inSituChatId = 'Loading...'; - String _tarballChatId = 'Loading...'; - String _riverInSituChatId = 'Loading...'; - String _riverTriennialChatId = 'Loading...'; - String _riverInvestigativeChatId = 'Loading...'; - String _airManualChatId = 'Loading...'; - String _airInvestigativeChatId = 'Loading...'; - String _marineInvestigativeChatId = 'Loading...'; + // REMOVED: Redundant state variable for settings sync + // bool _isSyncingSettings = false; + + // REMOVED: Chat ID state variables are no longer needed, + // we will read directly from the provider in the build method. final TextEditingController _tarballSearchController = TextEditingController(); String _tarballSearchQuery = ''; @@ -37,7 +34,7 @@ class _SettingsScreenState extends State { @override void initState() { super.initState(); - _loadCurrentSettings(); + // REMOVED: _loadCurrentSettings() is no longer needed as we read from the provider. _tarballSearchController.addListener(_onTarballSearchChanged); _manualSearchController.addListener(_onManualSearchChanged); _riverManualSearchController.addListener(_onRiverManualSearchChanged); @@ -53,31 +50,8 @@ class _SettingsScreenState extends State { super.dispose(); } - Future _loadCurrentSettings() async { - final results = await Future.wait([ - _settingsService.getInSituChatId(), - _settingsService.getTarballChatId(), - _settingsService.getRiverInSituChatId(), - _settingsService.getRiverTriennialChatId(), - _settingsService.getRiverInvestigativeChatId(), - _settingsService.getAirManualChatId(), - _settingsService.getAirInvestigativeChatId(), - _settingsService.getMarineInvestigativeChatId(), - ]); - - if (mounted) { - setState(() { - _inSituChatId = results[0].isNotEmpty ? results[0] : 'Not Set'; - _tarballChatId = results[1].isNotEmpty ? results[1] : 'Not Set'; - _riverInSituChatId = results[2].isNotEmpty ? results[2] : 'Not Set'; - _riverTriennialChatId = results[3].isNotEmpty ? results[3] : 'Not Set'; - _riverInvestigativeChatId = results[4].isNotEmpty ? results[4] : 'Not Set'; - _airManualChatId = results[5].isNotEmpty ? results[5] : 'Not Set'; - _airInvestigativeChatId = results[6].isNotEmpty ? results[6] : 'Not Set'; - _marineInvestigativeChatId = results[7].isNotEmpty ? results[7] : 'Not Set'; - }); - } - } + // REMOVED: _loadCurrentSettings is obsolete. The build method will now + // get the latest settings directly from AuthProvider. void _onTarballSearchChanged() { setState(() { _tarballSearchQuery = _tarballSearchController.text; }); @@ -102,6 +76,7 @@ class _SettingsScreenState extends State { final auth = Provider.of(context, listen: false); try { + // This now syncs ALL data, including settings. await auth.syncAllData(forceRefresh: true); if (mounted) { @@ -118,21 +93,8 @@ class _SettingsScreenState extends State { } } - Future _manualSettingsSync() async { - if (_isSyncingSettings) return; - setState(() => _isSyncingSettings = true); - - final success = await _settingsService.syncFromServer(); - - if (mounted) { - final message = success ? 'Telegram settings synced successfully.' : 'Failed to sync settings.'; - _showSnackBar(message, isError: !success); - if (success) { - await _loadCurrentSettings(); - } - setState(() => _isSyncingSettings = false); - } - } + // REMOVED: _manualSettingsSync is obsolete because the main data sync + // now handles settings as well. void _showSnackBar(String message, {bool isError = false}) { if (mounted) { @@ -150,6 +112,9 @@ class _SettingsScreenState extends State { final auth = Provider.of(context); final lastSync = auth.lastSyncTimestamp; + // Get the synced app settings from the provider. + final appSettings = auth.appSettings; + final filteredTarballStations = auth.tarballStations?.where((station) { final stationName = station['tbl_station_name']?.toLowerCase() ?? ''; final stationCode = station['tbl_station_code']?.toLowerCase() ?? ''; @@ -197,7 +162,7 @@ class _SettingsScreenState extends State { children: [ Text("Last Data Sync:", style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 4), - Text(lastSync != null ? DateFormat('yyyy-MM-dd HH:mm:ss').format(lastSync) : 'Never', style: Theme.of(context).textTheme.bodyLarge), + Text(lastSync != null ? DateFormat('yyyy-MM-dd HH:mm:ss').format(lastSync.toLocal()) : 'Never', style: Theme.of(context).textTheme.bodyLarge), const SizedBox(height: 16), ElevatedButton.icon( onPressed: _isSyncingData ? null : _manualDataSync, @@ -223,38 +188,29 @@ class _SettingsScreenState extends State { title: const Text('Marine Alerts', style: TextStyle(fontWeight: FontWeight.bold)), initiallyExpanded: false, children: [ - _buildChatIdEntry('In-Situ', _inSituChatId), - _buildChatIdEntry('Tarball', _tarballChatId), - _buildChatIdEntry('Investigative', _marineInvestigativeChatId), + _buildChatIdEntry('In-Situ', _settingsService.getInSituChatId(appSettings)), + _buildChatIdEntry('Tarball', _settingsService.getTarballChatId(appSettings)), + _buildChatIdEntry('Investigative', _settingsService.getMarineInvestigativeChatId(appSettings)), ], ), ExpansionTile( title: const Text('River Alerts', style: TextStyle(fontWeight: FontWeight.bold)), initiallyExpanded: false, children: [ - _buildChatIdEntry('In-Situ', _riverInSituChatId), - _buildChatIdEntry('Triennial', _riverTriennialChatId), - _buildChatIdEntry('Investigative', _riverInvestigativeChatId), + _buildChatIdEntry('In-Situ', _settingsService.getRiverInSituChatId(appSettings)), + _buildChatIdEntry('Triennial', _settingsService.getRiverTriennialChatId(appSettings)), + _buildChatIdEntry('Investigative', _settingsService.getRiverInvestigativeChatId(appSettings)), ], ), ExpansionTile( title: const Text('Air Alerts', style: TextStyle(fontWeight: FontWeight.bold)), initiallyExpanded: false, children: [ - _buildChatIdEntry('Manual', _airManualChatId), - _buildChatIdEntry('Investigative', _airInvestigativeChatId), + _buildChatIdEntry('Manual', _settingsService.getAirManualChatId(appSettings)), + _buildChatIdEntry('Investigative', _settingsService.getAirInvestigativeChatId(appSettings)), ], ), - const SizedBox(height: 16), - ElevatedButton.icon( - onPressed: _isSyncingSettings ? null : _manualSettingsSync, - icon: _isSyncingSettings ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Icon(Icons.settings_backup_restore), - label: Text(_isSyncingSettings ? 'Syncing Settings...' : 'Sync Telegram Settings'), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.secondary, - minimumSize: const Size(double.infinity, 50), - ), - ), + // REMOVED: The separate sync button for settings is no longer needed. ], ), ), @@ -476,7 +432,7 @@ class _SettingsScreenState extends State { contentPadding: EdgeInsets.zero, leading: const Icon(Icons.telegram, size: 20), title: Text('$label Chat ID'), - subtitle: Text(value), + subtitle: Text(value.isNotEmpty ? value : 'Not Set'), dense: true, ); } diff --git a/lib/services/air_sampling_service.dart b/lib/services/air_sampling_service.dart index 9218619..1683c03 100644 --- a/lib/services/air_sampling_service.dart +++ b/lib/services/air_sampling_service.dart @@ -70,24 +70,28 @@ class AirSamplingService { return File(filePath)..writeAsBytesSync(img.encodeJpg(originalImage)); } - Future _handleInstallationSuccessAlert(AirInstallationData data, {required bool isDataOnly}) async { + // MODIFIED: Method now requires the appSettings list to pass to TelegramService. + Future _handleInstallationSuccessAlert(AirInstallationData data, List>? appSettings, {required bool isDataOnly}) async { try { final message = data.generateInstallationTelegramAlert(isDataOnly: isDataOnly); - final bool wasSent = await _telegramService.sendAlertImmediately('air_manual', message); + // Pass the appSettings list to the telegram service methods + final bool wasSent = await _telegramService.sendAlertImmediately('air_manual', message, appSettings); if (!wasSent) { - await _telegramService.queueMessage('air_manual', message); + await _telegramService.queueMessage('air_manual', message, appSettings); } } catch (e) { debugPrint("Failed to handle Air Manual Installation Telegram alert: $e"); } } - Future _handleCollectionSuccessAlert(AirCollectionData data, AirInstallationData installationData, {required bool isDataOnly}) async { + // MODIFIED: Method now requires the appSettings list to pass to TelegramService. + Future _handleCollectionSuccessAlert(AirCollectionData data, AirInstallationData installationData, List>? appSettings, {required bool isDataOnly}) async { try { final message = data.generateCollectionTelegramAlert(installationData, isDataOnly: isDataOnly); - final bool wasSent = await _telegramService.sendAlertImmediately('air_manual', message); + // Pass the appSettings list to the telegram service methods + final bool wasSent = await _telegramService.sendAlertImmediately('air_manual', message, appSettings); if (!wasSent) { - await _telegramService.queueMessage('air_manual', message); + await _telegramService.queueMessage('air_manual', message, appSettings); } } catch (e) { debugPrint("Failed to handle Air Manual Collection Telegram alert: $e"); @@ -95,7 +99,8 @@ class AirSamplingService { } /// Orchestrates a two-step submission process for air installation samples. - Future> submitInstallation(AirInstallationData data) async { + // MODIFIED: Method now requires the appSettings list to pass down the call stack. + Future> submitInstallation(AirInstallationData data, List>? appSettings) async { // --- OFFLINE-FIRST HELPER --- Future> saveLocally(String status, String message) async { debugPrint("Saving installation locally with status: $status"); @@ -107,7 +112,7 @@ class AirSamplingService { // If the record's text data is already on the server, skip directly to image upload. if (data.status == 'L2_PENDING_IMAGES' && data.airManId != null) { debugPrint("Retrying image upload for existing record ID: ${data.airManId}"); - return await _uploadInstallationImagesAndUpdate(data); + return await _uploadInstallationImagesAndUpdate(data, appSettings); } // --- STEP 1: SUBMIT TEXT DATA --- @@ -137,17 +142,18 @@ class AirSamplingService { data.airManId = parsedRecordId; // --- STEP 2: UPLOAD IMAGE FILES --- - return await _uploadInstallationImagesAndUpdate(data); + return await _uploadInstallationImagesAndUpdate(data, appSettings); } /// A reusable function for handling the image upload and local data update logic. - Future> _uploadInstallationImagesAndUpdate(AirInstallationData data) async { + // MODIFIED: Method now requires the appSettings list to pass to the alert handler. + Future> _uploadInstallationImagesAndUpdate(AirInstallationData data, List>? appSettings) async { final filesToUpload = data.getImagesForUpload(); if (filesToUpload.isEmpty) { debugPrint("No images to upload. Submission complete."); data.status = 'S1'; // Server Pending (no images needed) await _localStorageService.saveAirSamplingRecord(data.toMap(), data.refID!); - _handleInstallationSuccessAlert(data, isDataOnly: true); + _handleInstallationSuccessAlert(data, appSettings, isDataOnly: true); return {'status': 'S1', 'message': 'Installation data submitted successfully.'}; } @@ -170,7 +176,7 @@ class AirSamplingService { debugPrint("Images uploaded successfully."); data.status = 'S2'; // Server Pending (images uploaded) await _localStorageService.saveAirSamplingRecord(data.toMap(), data.refID!); - _handleInstallationSuccessAlert(data, isDataOnly: false); + _handleInstallationSuccessAlert(data, appSettings, isDataOnly: false); return { 'status': 'S2', 'message': 'Installation data and images submitted successfully.', @@ -178,7 +184,8 @@ class AirSamplingService { } /// Submits only the collection data, linked to a previous installation. - Future> submitCollection(AirCollectionData data, AirInstallationData installationData) async { + // MODIFIED: Method now requires the appSettings list to pass down the call stack. + Future> submitCollection(AirCollectionData data, AirInstallationData installationData, List>? appSettings) async { // --- OFFLINE-FIRST HELPER (CORRECTED) --- Future> updateAndSaveLocally(String newStatus, {String? message}) async { debugPrint("Saving collection data locally with status: $newStatus"); @@ -201,7 +208,7 @@ class AirSamplingService { // If the record's text data is already on the server, skip directly to image upload. if (data.status == 'L4_PENDING_IMAGES' && data.airManId != null) { debugPrint("Retrying collection image upload for existing record ID: ${data.airManId}"); - return await _uploadCollectionImagesAndUpdate(data, installationData); + return await _uploadCollectionImagesAndUpdate(data, installationData, appSettings); } // --- STEP 1: SUBMIT TEXT DATA --- @@ -215,11 +222,12 @@ class AirSamplingService { debugPrint("Collection text data submitted successfully."); // --- STEP 2: UPLOAD IMAGE FILES --- - return await _uploadCollectionImagesAndUpdate(data, installationData); + return await _uploadCollectionImagesAndUpdate(data, installationData, appSettings); } /// A reusable function for handling the collection image upload and local data update logic. - Future> _uploadCollectionImagesAndUpdate(AirCollectionData data, AirInstallationData installationData) async { + // MODIFIED: Method now requires the appSettings list to pass to the alert handler. + Future> _uploadCollectionImagesAndUpdate(AirCollectionData data, AirInstallationData installationData, List>? appSettings) async { // --- OFFLINE-FIRST HELPER (CORRECTED) --- Future> updateAndSaveLocally(String newStatus, {String? message}) async { debugPrint("Saving collection data locally with status: $newStatus"); @@ -242,7 +250,7 @@ class AirSamplingService { if (filesToUpload.isEmpty) { debugPrint("No collection images to upload. Submission complete."); await updateAndSaveLocally('S3'); // S3 = Server Completed - _handleCollectionSuccessAlert(data, installationData, isDataOnly: true); + _handleCollectionSuccessAlert(data, installationData, appSettings, isDataOnly: true); return {'status': 'S3', 'message': 'Collection data submitted successfully.'}; } @@ -260,7 +268,7 @@ class AirSamplingService { debugPrint("Images uploaded successfully."); await updateAndSaveLocally('S3'); // S3 = Server Completed - _handleCollectionSuccessAlert(data, installationData, isDataOnly: false); + _handleCollectionSuccessAlert(data, installationData, appSettings, isDataOnly: false); return { 'status': 'S3', 'message': 'Collection data and images submitted successfully.', @@ -291,4 +299,4 @@ class AirSamplingService { void dispose() { // Clean up any resources if necessary } -} +} \ No newline at end of file diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index b8485a2..0ddd9c8 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -33,7 +33,7 @@ class ApiService { air = AirApiService(_baseService); } - // --- Core API Methods --- + // --- Core API Methods (Unchanged) --- Future> login(String email, String password) { return _baseService.post('auth/login', {'email': email, 'password': password}); @@ -121,79 +121,87 @@ class ApiService { return result; } + // --- REWRITTEN FOR DELTA SYNC --- - /// Orchestrates a full data sync from the server to the local database. - Future> syncAllData() async { - debugPrint('ApiService: Starting full data sync from server...'); + /// Helper method to make a delta-sync API call. + Future> _fetchDelta(String endpoint, String? lastSyncTimestamp) { + String url = endpoint; + if (lastSyncTimestamp != null) { + // Append the 'since' parameter to the URL for delta requests + url += '?since=$lastSyncTimestamp'; + } + return _baseService.get(url); + } + + /// Orchestrates a full DELTA sync from the server to the local database. + Future> syncAllData({String? lastSyncTimestamp}) async { + debugPrint('ApiService: Starting DELTA data sync. Since: $lastSyncTimestamp'); try { - final results = await Future.wait([ - getProfile(), - getAllUsers(), - marine.getTarballStations(), - marine.getManualStations(), - marine.getTarballClassifications(), - river.getManualStations(), - river.getTriennialStations(), - getAllDepartments(), - getAllCompanies(), - getAllPositions(), - air.getManualStations(), - air.getClients(), - getAllStates(), - ]); - - final Map syncedData = { - 'profile': results[0]['success'] == true ? results[0]['data'] : null, - 'allUsers': results[1]['success'] == true ? results[1]['data'] : null, - 'tarballStations': results[2]['success'] == true ? results[2]['data'] : null, - 'manualStations': results[3]['success'] == true ? results[3]['data'] : null, - 'tarballClassifications': results[4]['success'] == true ? results[4]['data'] : null, - 'riverManualStations': results[5]['success'] == true ? results[5]['data'] : null, - 'riverTriennialStations': results[6]['success'] == true ? results[6]['data'] : null, - 'departments': results[7]['success'] == true ? results[7]['data'] : null, - 'companies': results[8]['success'] == true ? results[8]['data'] : null, - 'positions': results[9]['success'] == true ? results[9]['data'] : null, - 'airManualStations': results[10]['success'] == true ? results[10]['data'] : null, - 'airClients': results[11]['success'] == true ? results[11]['data'] : null, - 'states': results[12]['success'] == true ? results[12]['data'] : null, + // Defines all data types to sync, their endpoints, and their DB handlers. + final syncTasks = { + 'profile': {'endpoint': 'profile', 'handler': (d, id) async { if (d.isNotEmpty) await _dbHelper.saveProfile(d.first); }}, + 'allUsers': {'endpoint': 'users', 'handler': (d, id) async { await _dbHelper.upsertUsers(d); await _dbHelper.deleteUsers(id); }}, + 'tarballStations': {'endpoint': 'marine/tarball/stations', 'handler': (d, id) async { await _dbHelper.upsertTarballStations(d); await _dbHelper.deleteTarballStations(id); }}, + 'manualStations': {'endpoint': 'marine/manual/stations', 'handler': (d, id) async { await _dbHelper.upsertManualStations(d); await _dbHelper.deleteManualStations(id); }}, + 'tarballClassifications': {'endpoint': 'marine/tarball/classifications', 'handler': (d, id) async { await _dbHelper.upsertTarballClassifications(d); await _dbHelper.deleteTarballClassifications(id); }}, + 'riverManualStations': {'endpoint': 'river/manual-stations', 'handler': (d, id) async { await _dbHelper.upsertRiverManualStations(d); await _dbHelper.deleteRiverManualStations(id); }}, + 'riverTriennialStations': {'endpoint': 'river/triennial-stations', 'handler': (d, id) async { await _dbHelper.upsertRiverTriennialStations(d); await _dbHelper.deleteRiverTriennialStations(id); }}, + 'departments': {'endpoint': 'departments', 'handler': (d, id) async { await _dbHelper.upsertDepartments(d); await _dbHelper.deleteDepartments(id); }}, + 'companies': {'endpoint': 'companies', 'handler': (d, id) async { await _dbHelper.upsertCompanies(d); await _dbHelper.deleteCompanies(id); }}, + 'positions': {'endpoint': 'positions', 'handler': (d, id) async { await _dbHelper.upsertPositions(d); await _dbHelper.deletePositions(id); }}, + 'airManualStations': {'endpoint': 'air/manual-stations', 'handler': (d, id) async { await _dbHelper.upsertAirManualStations(d); await _dbHelper.deleteAirManualStations(id); }}, + 'airClients': {'endpoint': 'air/clients', 'handler': (d, id) async { await _dbHelper.upsertAirClients(d); await _dbHelper.deleteAirClients(id); }}, + 'states': {'endpoint': 'states', 'handler': (d, id) async { await _dbHelper.upsertStates(d); await _dbHelper.deleteStates(id); }}, + 'appSettings': {'endpoint': 'settings', 'handler': (d, id) async { await _dbHelper.upsertAppSettings(d); await _dbHelper.deleteAppSettings(id); }}, + 'parameterLimits': {'endpoint': 'parameter-limits', 'handler': (d, id) async { await _dbHelper.upsertParameterLimits(d); await _dbHelper.deleteParameterLimits(id); }}, }; - if (syncedData['profile'] != null) await _dbHelper.saveProfile(syncedData['profile']); - if (syncedData['allUsers'] != null) await _dbHelper.saveUsers(List>.from(syncedData['allUsers'])); - if (syncedData['tarballStations'] != null) await _dbHelper.saveTarballStations(List>.from(syncedData['tarballStations'])); - if (syncedData['manualStations'] != null) await _dbHelper.saveManualStations(List>.from(syncedData['manualStations'])); - if (syncedData['tarballClassifications'] != null) await _dbHelper.saveTarballClassifications(List>.from(syncedData['tarballClassifications'])); - if (syncedData['riverManualStations'] != null) await _dbHelper.saveRiverManualStations(List>.from(syncedData['riverManualStations'])); - if (syncedData['riverTriennialStations'] != null) await _dbHelper.saveRiverTriennialStations(List>.from(syncedData['riverTriennialStations'])); - if (syncedData['departments'] != null) await _dbHelper.saveDepartments(List>.from(syncedData['departments'])); - if (syncedData['companies'] != null) await _dbHelper.saveCompanies(List>.from(syncedData['companies'])); - if (syncedData['positions'] != null) await _dbHelper.savePositions(List>.from(syncedData['positions'])); - if (syncedData['airManualStations'] != null) await _dbHelper.saveAirManualStations(List>.from(syncedData['airManualStations'])); - if (syncedData['airClients'] != null) await _dbHelper.saveAirClients(List>.from(syncedData['airClients'])); - if (syncedData['states'] != null) await _dbHelper.saveStates(List>.from(syncedData['states'])); + // Fetch all deltas in parallel + final fetchFutures = syncTasks.map((key, value) => MapEntry(key, _fetchDelta(value['endpoint'] as String, lastSyncTimestamp))); + final results = await Future.wait(fetchFutures.values); + final resultData = Map.fromIterables(fetchFutures.keys, results); - debugPrint('ApiService: Sync complete. Data saved to local DB.'); - return {'success': true, 'data': syncedData}; + // Process and save all changes + for (var entry in resultData.entries) { + final key = entry.key; + final result = entry.value; + + if (result['success'] == true && result['data'] != null) { + // The profile endpoint has a different structure, handle it separately. + if (key == 'profile') { + await (syncTasks[key]!['handler'] as Function)([result['data']], []); + } else { + final updated = List>.from(result['data']['updated'] ?? []); + final deleted = List.from(result['data']['deleted'] ?? []); + await (syncTasks[key]!['handler'] as Function)(updated, deleted); + } + } else { + debugPrint('ApiService: Failed to sync $key. Message: ${result['message']}'); + } + } + + debugPrint('ApiService: Delta sync complete.'); + return {'success': true, 'message': 'Delta sync successful.'}; } catch (e) { - debugPrint('ApiService: Full data sync failed: $e'); + debugPrint('ApiService: Delta data sync failed: $e'); return {'success': false, 'message': 'Data sync failed: $e'}; } } } // ======================================================================= -// Part 2: Feature-Specific API Services +// Part 2: Feature-Specific API Services (Unchanged) // ======================================================================= class AirApiService { +// ... (No changes needed here) final BaseApiService _baseService; AirApiService(this._baseService); Future> getManualStations() => _baseService.get('air/manual-stations'); Future> getClients() => _baseService.get('air/clients'); - // NEW: Added dedicated method for uploading installation images Future> uploadInstallationImages({ required String airManId, required Map files, @@ -205,13 +213,11 @@ class AirApiService { ); } - // NECESSARY FIX: Added dedicated method for uploading collection images Future> uploadCollectionImages({ required String airManId, required Map files, }) { return _baseService.postMultipart( - // Note: Please verify this endpoint path with your backend developer. endpoint: 'air/manual/collection-images', fields: {'air_man_id': airManId}, files: files, @@ -221,6 +227,7 @@ class AirApiService { class MarineApiService { +// ... (No changes needed here) final BaseApiService _baseService; MarineApiService(this._baseService); @@ -251,6 +258,7 @@ class MarineApiService { } class RiverApiService { +// ... (No changes needed here) final BaseApiService _baseService; RiverApiService(this._baseService); @@ -259,13 +267,14 @@ class RiverApiService { } // ======================================================================= -// Part 3: Local Database Helper +// Part 3: Local Database Helper (Refactored for Delta Sync) // ======================================================================= class DatabaseHelper { static Database? _database; static const String _dbName = 'app_data.db'; - static const int _dbVersion = 12; + // Incremented DB version to trigger the onUpgrade method + static const int _dbVersion = 13; static const String _profileTable = 'user_profile'; static const String _usersTable = 'all_users'; @@ -281,7 +290,9 @@ class DatabaseHelper { static const String _airManualStationsTable = 'air_manual_stations'; static const String _airClientsTable = 'air_clients'; static const String _statesTable = 'states'; - + // Added new table constants + static const String _appSettingsTable = 'app_settings'; + static const String _parameterLimitsTable = 'manual_parameter_limits'; Future get database async { if (_database != null) return _database!; @@ -309,6 +320,9 @@ class DatabaseHelper { await db.execute('CREATE TABLE $_airManualStationsTable(station_id INTEGER PRIMARY KEY, station_json TEXT)'); await db.execute('CREATE TABLE $_airClientsTable(client_id INTEGER PRIMARY KEY, client_json TEXT)'); await db.execute('CREATE TABLE $_statesTable(state_id INTEGER PRIMARY KEY, state_json TEXT)'); + // Added create statements for new tables + await db.execute('CREATE TABLE $_appSettingsTable(setting_id INTEGER PRIMARY KEY, setting_json TEXT)'); + await db.execute('CREATE TABLE $_parameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)'); } Future _onUpgrade(Database db, int oldVersion, int newVersion) async { @@ -319,21 +333,46 @@ class DatabaseHelper { if (oldVersion < 12) { await db.execute('CREATE TABLE IF NOT EXISTS $_statesTable(state_id INTEGER PRIMARY KEY, state_json TEXT)'); } - } - - Future _saveData(String table, String idKey, List> data) async { - final db = await database; - await db.delete(table); - for (var item in data) { - await db.insert(table, {'${idKey}_id': item['${idKey}_id'], '${idKey}_json': jsonEncode(item)}, conflictAlgorithm: ConflictAlgorithm.replace); + if (oldVersion < 13) { + await db.execute('CREATE TABLE IF NOT EXISTS $_appSettingsTable(setting_id INTEGER PRIMARY KEY, setting_json TEXT)'); + await db.execute('CREATE TABLE IF NOT EXISTS $_parameterLimitsTable(param_autoid INTEGER PRIMARY KEY, limit_json TEXT)'); } } - Future>?> _loadData(String table, String idKey) async { + /// Performs an "upsert": inserts new records or replaces existing ones. + Future _upsertData(String table, String idKeyName, List> data, String jsonKeyName) async { + if (data.isEmpty) return; + final db = await database; + final batch = db.batch(); + for (var item in data) { + batch.insert( + table, + {idKeyName: item[idKeyName], '${jsonKeyName}_json': jsonEncode(item)}, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + await batch.commit(noResult: true); + debugPrint("Upserted ${data.length} items into $table"); + } + + /// Deletes a list of records from a table by their primary keys. + Future _deleteData(String table, String idKeyName, List ids) async { + if (ids.isEmpty) return; + final db = await database; + final placeholders = List.filled(ids.length, '?').join(', '); + await db.delete( + table, + where: '$idKeyName IN ($placeholders)', + whereArgs: ids, + ); + debugPrint("Deleted ${ids.length} items from $table"); + } + + Future>?> _loadData(String table, String jsonKey) async { final db = await database; final List> maps = await db.query(table); if (maps.isNotEmpty) { - return maps.map((map) => jsonDecode(map['${idKey}_json']) as Map).toList(); + return maps.map((map) => jsonDecode(map['${jsonKey}_json']) as Map).toList(); } return null; } @@ -349,39 +388,61 @@ class DatabaseHelper { return null; } - Future saveUsers(List> users) => _saveData(_usersTable, 'user', users); + // --- Upsert/Delete/Load methods for all data types --- + + Future upsertUsers(List> data) => _upsertData(_usersTable, 'user_id', data, 'user'); + Future deleteUsers(List ids) => _deleteData(_usersTable, 'user_id', ids); Future>?> loadUsers() => _loadData(_usersTable, 'user'); - Future saveTarballStations(List> stations) => _saveData(_tarballStationsTable, 'station', stations); + Future upsertTarballStations(List> data) => _upsertData(_tarballStationsTable, 'station_id', data, 'station'); + Future deleteTarballStations(List ids) => _deleteData(_tarballStationsTable, 'station_id', ids); Future>?> loadTarballStations() => _loadData(_tarballStationsTable, 'station'); - Future saveManualStations(List> stations) => _saveData(_manualStationsTable, 'station', stations); + Future upsertManualStations(List> data) => _upsertData(_manualStationsTable, 'station_id', data, 'station'); + Future deleteManualStations(List ids) => _deleteData(_manualStationsTable, 'station_id', ids); Future>?> loadManualStations() => _loadData(_manualStationsTable, 'station'); - Future saveRiverManualStations(List> stations) => _saveData(_riverManualStationsTable, 'station', stations); + Future upsertRiverManualStations(List> data) => _upsertData(_riverManualStationsTable, 'station_id', data, 'station'); + Future deleteRiverManualStations(List ids) => _deleteData(_riverManualStationsTable, 'station_id', ids); Future>?> loadRiverManualStations() => _loadData(_riverManualStationsTable, 'station'); - Future saveRiverTriennialStations(List> stations) => _saveData(_riverTriennialStationsTable, 'station', stations); + Future upsertRiverTriennialStations(List> data) => _upsertData(_riverTriennialStationsTable, 'station_id', data, 'station'); + Future deleteRiverTriennialStations(List ids) => _deleteData(_riverTriennialStationsTable, 'station_id', ids); Future>?> loadRiverTriennialStations() => _loadData(_riverTriennialStationsTable, 'station'); - Future saveTarballClassifications(List> data) => _saveData(_tarballClassificationsTable, 'classification', data); + Future upsertTarballClassifications(List> data) => _upsertData(_tarballClassificationsTable, 'classification_id', data, 'classification'); + Future deleteTarballClassifications(List ids) => _deleteData(_tarballClassificationsTable, 'classification_id', ids); Future>?> loadTarballClassifications() => _loadData(_tarballClassificationsTable, 'classification'); - Future saveDepartments(List> data) => _saveData(_departmentsTable, 'department', data); + Future upsertDepartments(List> data) => _upsertData(_departmentsTable, 'department_id', data, 'department'); + Future deleteDepartments(List ids) => _deleteData(_departmentsTable, 'department_id', ids); Future>?> loadDepartments() => _loadData(_departmentsTable, 'department'); - Future saveCompanies(List> data) => _saveData(_companiesTable, 'company', data); + Future upsertCompanies(List> data) => _upsertData(_companiesTable, 'company_id', data, 'company'); + Future deleteCompanies(List ids) => _deleteData(_companiesTable, 'company_id', ids); Future>?> loadCompanies() => _loadData(_companiesTable, 'company'); - Future savePositions(List> data) => _saveData(_positionsTable, 'position', data); + Future upsertPositions(List> data) => _upsertData(_positionsTable, 'position_id', data, 'position'); + Future deletePositions(List ids) => _deleteData(_positionsTable, 'position_id', ids); Future>?> loadPositions() => _loadData(_positionsTable, 'position'); - Future saveAirManualStations(List> stations) => _saveData(_airManualStationsTable, 'station', stations); + Future upsertAirManualStations(List> data) => _upsertData(_airManualStationsTable, 'station_id', data, 'station'); + Future deleteAirManualStations(List ids) => _deleteData(_airManualStationsTable, 'station_id', ids); Future>?> loadAirManualStations() => _loadData(_airManualStationsTable, 'station'); - Future saveAirClients(List> clients) => _saveData(_airClientsTable, 'client', clients); + Future upsertAirClients(List> data) => _upsertData(_airClientsTable, 'client_id', data, 'client'); + Future deleteAirClients(List ids) => _deleteData(_airClientsTable, 'client_id', ids); Future>?> loadAirClients() => _loadData(_airClientsTable, 'client'); - Future saveStates(List> states) => _saveData(_statesTable, 'state', states); + Future upsertStates(List> data) => _upsertData(_statesTable, 'state_id', data, 'state'); + Future deleteStates(List ids) => _deleteData(_statesTable, 'state_id', ids); Future>?> loadStates() => _loadData(_statesTable, 'state'); + + Future upsertAppSettings(List> data) => _upsertData(_appSettingsTable, 'setting_id', data, 'setting'); + Future deleteAppSettings(List ids) => _deleteData(_appSettingsTable, 'setting_id', ids); + Future>?> loadAppSettings() => _loadData(_appSettingsTable, 'setting'); + + Future upsertParameterLimits(List> data) => _upsertData(_parameterLimitsTable, 'param_autoid', data, 'limit'); + Future deleteParameterLimits(List ids) => _deleteData(_parameterLimitsTable, 'param_autoid', ids); + Future>?> loadParameterLimits() => _loadData(_parameterLimitsTable, 'limit'); } \ No newline at end of file diff --git a/lib/services/in_situ_sampling_service.dart b/lib/services/in_situ_sampling_service.dart index a2fc8b0..1a417e9 100644 --- a/lib/services/in_situ_sampling_service.dart +++ b/lib/services/in_situ_sampling_service.dart @@ -142,11 +142,13 @@ class InSituSamplingService { } // --- Data Submission --- - Future> submitData(InSituSamplingData data) { + // MODIFIED: Method now requires the appSettings list to pass to the MarineApiService. + Future> submitData(InSituSamplingData data, List>? appSettings) { return _marineApiService.submitInSituSample( formData: data.toApiFormData(), imageFiles: data.toApiImageFiles(), - inSituData: data, // Added this required parameter + inSituData: data, + appSettings: appSettings, // Added this required parameter ); } } \ No newline at end of file diff --git a/lib/services/marine_api_service.dart b/lib/services/marine_api_service.dart index 4e0834b..b1bd074 100644 --- a/lib/services/marine_api_service.dart +++ b/lib/services/marine_api_service.dart @@ -10,7 +10,8 @@ import 'package:environment_monitoring_app/models/tarball_data.dart'; class MarineApiService { final BaseApiService _baseService = BaseApiService(); final TelegramService _telegramService = TelegramService(); - final SettingsService _settingsService = SettingsService(); + // REMOVED: SettingsService is no longer called directly from this file for chat IDs. + // final SettingsService _settingsService = SettingsService(); Future> getTarballStations() { return _baseService.get('marine/tarball/stations'); @@ -24,9 +25,11 @@ class MarineApiService { return _baseService.get('marine/tarball/classifications'); } + // MODIFIED: Method now requires the appSettings list. Future> submitTarballSample({ required Map formData, required Map imageFiles, + required List>? appSettings, }) async { debugPrint("Step 1: Submitting tarball form data to the server..."); final dataResult = await _baseService.post('marine/tarball/sample', formData); @@ -57,7 +60,7 @@ class MarineApiService { }); if (filesToUpload.isEmpty) { - _handleTarballSuccessAlert(formData, isDataOnly: true); + _handleTarballSuccessAlert(formData, appSettings, isDataOnly: true); return { 'status': 'L3', 'success': true, @@ -82,7 +85,7 @@ class MarineApiService { }; } - _handleTarballSuccessAlert(formData, isDataOnly: false); + _handleTarballSuccessAlert(formData, appSettings, isDataOnly: false); return { 'status': 'L3', 'success': true, @@ -91,10 +94,12 @@ class MarineApiService { }; } + // MODIFIED: Method now requires the appSettings list. Future> submitInSituSample({ required Map formData, required Map imageFiles, required InSituSamplingData inSituData, + required List>? appSettings, }) async { debugPrint("Step 1: Submitting in-situ form data to the server..."); final dataResult = await _baseService.post('marine/manual/sample', formData); @@ -125,7 +130,7 @@ class MarineApiService { }); if (filesToUpload.isEmpty) { - _handleInSituSuccessAlert(inSituData, isDataOnly: true); + _handleInSituSuccessAlert(inSituData, appSettings, isDataOnly: true); return { 'status': 'L3', 'success': true, @@ -150,7 +155,7 @@ class MarineApiService { }; } - _handleInSituSuccessAlert(inSituData, isDataOnly: false); + _handleInSituSuccessAlert(inSituData, appSettings, isDataOnly: false); return { 'status': 'L3', 'success': true, @@ -159,15 +164,13 @@ class MarineApiService { }; } - Future _handleTarballSuccessAlert(Map formData, {required bool isDataOnly}) async { + // MODIFIED: Method now requires appSettings and calls the updated TelegramService. + Future _handleTarballSuccessAlert(Map formData, List>? appSettings, {required bool isDataOnly}) async { try { - final groupChatId = await _settingsService.getTarballChatId(); - if (groupChatId.isNotEmpty) { - final message = _generateTarballAlertMessage(formData, isDataOnly: isDataOnly); - final bool wasSent = await _telegramService.sendAlertImmediately('marine_tarball', message); - if (!wasSent) { - await _telegramService.queueMessage('marine_tarball', message); - } + final message = _generateTarballAlertMessage(formData, isDataOnly: isDataOnly); + final bool wasSent = await _telegramService.sendAlertImmediately('marine_tarball', message, appSettings); + if (!wasSent) { + await _telegramService.queueMessage('marine_tarball', message, appSettings); } } catch (e) { debugPrint("Failed to handle Tarball Telegram alert: $e"); @@ -205,15 +208,13 @@ class MarineApiService { return buffer.toString(); } - Future _handleInSituSuccessAlert(InSituSamplingData data, {required bool isDataOnly}) async { + // MODIFIED: Method now requires appSettings and calls the updated TelegramService. + Future _handleInSituSuccessAlert(InSituSamplingData data, List>? appSettings, {required bool isDataOnly}) async { try { - final groupChatId = await _settingsService.getInSituChatId(); - if (groupChatId.isNotEmpty) { - final message = data.generateTelegramAlertMessage(isDataOnly: isDataOnly); - final bool wasSent = await _telegramService.sendAlertImmediately('marine_in_situ', message); - if (!wasSent) { - await _telegramService.queueMessage('marine_in_situ', message); - } + final message = data.generateTelegramAlertMessage(isDataOnly: isDataOnly); + final bool wasSent = await _telegramService.sendAlertImmediately('marine_in_situ', message, appSettings); + if (!wasSent) { + await _telegramService.queueMessage('marine_in_situ', message, appSettings); } } catch (e) { debugPrint("Failed to handle In-Situ Telegram alert: $e"); diff --git a/lib/services/river_api_service.dart b/lib/services/river_api_service.dart index 59fd7f3..8344c5e 100644 --- a/lib/services/river_api_service.dart +++ b/lib/services/river_api_service.dart @@ -6,12 +6,14 @@ import 'package:intl/intl.dart'; import 'package:environment_monitoring_app/services/base_api_service.dart'; import 'package:environment_monitoring_app/services/telegram_service.dart'; -import 'package:environment_monitoring_app/services/settings_service.dart'; +// REMOVED: SettingsService is no longer needed in this file. +// import 'package:environment_monitoring_app/services/settings_service.dart'; class RiverApiService { final BaseApiService _baseService = BaseApiService(); final TelegramService _telegramService = TelegramService(); - final SettingsService _settingsService = SettingsService(); + // REMOVED: SettingsService instance is no longer needed. + // final SettingsService _settingsService = SettingsService(); Future> getManualStations() { return _baseService.get('river/manual-stations'); @@ -21,9 +23,11 @@ class RiverApiService { return _baseService.get('river/triennial-stations'); } + // MODIFIED: Method now requires the appSettings list to pass to the alert handler. Future> submitInSituSample({ required Map formData, required Map imageFiles, + required List>? appSettings, }) async { // --- Step 1: Submit Form Data as JSON --- // The PHP backend for submitInSituSample expects JSON input. @@ -55,7 +59,7 @@ class RiverApiService { }); if (filesToUpload.isEmpty) { - _handleInSituSuccessAlert(formData, isDataOnly: true); + _handleInSituSuccessAlert(formData, appSettings, isDataOnly: true); return { 'status': 'L3', 'success': true, @@ -79,7 +83,7 @@ class RiverApiService { }; } - _handleInSituSuccessAlert(formData, isDataOnly: false); + _handleInSituSuccessAlert(formData, appSettings, isDataOnly: false); return { 'status': 'L3', 'success': true, @@ -88,45 +92,44 @@ class RiverApiService { }; } - Future _handleInSituSuccessAlert(Map formData, {required bool isDataOnly}) async { + // MODIFIED: Method now requires appSettings and calls the updated TelegramService. + Future _handleInSituSuccessAlert(Map formData, List>? appSettings, {required bool isDataOnly}) async { try { - final groupChatId = await _settingsService.getInSituChatId(); - if (groupChatId.isNotEmpty) { - final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)"; - final stationName = formData['r_man_station_name'] ?? 'N/A'; - final stationCode = formData['r_man_station_code'] ?? 'N/A'; - final submissionDate = formData['r_man_date'] ?? DateFormat('yyyy-MM-dd').format(DateTime.now()); - final submitter = formData['first_sampler_name'] ?? 'N/A'; - final sondeID = formData['r_man_sondeID'] ?? 'N/A'; - final distanceKm = double.tryParse(formData['r_man_distance_difference'] ?? '0') ?? 0; - final distanceMeters = (distanceKm * 1000).toStringAsFixed(0); - final distanceRemarks = formData['r_man_distance_difference_remarks'] ?? 'N/A'; + final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)"; + final stationName = formData['r_man_station_name'] ?? 'N/A'; + final stationCode = formData['r_man_station_code'] ?? 'N/A'; + final submissionDate = formData['r_man_date'] ?? DateFormat('yyyy-MM-dd').format(DateTime.now()); + final submitter = formData['first_sampler_name'] ?? 'N/A'; + final sondeID = formData['r_man_sondeID'] ?? 'N/A'; + final distanceKm = double.tryParse(formData['r_man_distance_difference'] ?? '0') ?? 0; + final distanceMeters = (distanceKm * 1000).toStringAsFixed(0); + final distanceRemarks = formData['r_man_distance_difference_remarks'] ?? 'N/A'; - final buffer = StringBuffer() - ..writeln('✅ *River In-Situ Sample ${submissionType} Submitted:*') + final buffer = StringBuffer() + ..writeln('✅ *River In-Situ Sample ${submissionType} Submitted:*') + ..writeln() + ..writeln('*Station Name & Code:* $stationName ($stationCode)') + ..writeln('*Date of Submitted:* $submissionDate') + ..writeln('*Submitted by User:* $submitter') + ..writeln('*Sonde ID:* $sondeID') + ..writeln('*Status of Submission:* Successful'); + + if (distanceKm > 0 || (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A')) { + buffer ..writeln() - ..writeln('*Station Name & Code:* $stationName ($stationCode)') - ..writeln('*Date of Submitted:* $submissionDate') - ..writeln('*Submitted by User:* $submitter') - ..writeln('*Sonde ID:* $sondeID') - ..writeln('*Status of Submission:* Successful'); - - if (distanceKm > 0 || (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A')) { - buffer - ..writeln() - ..writeln('🔔 *Alert:*') - ..writeln('*Distance from station:* $distanceMeters meters'); - if (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A') { - buffer.writeln('*Remarks for distance:* $distanceRemarks'); - } + ..writeln('🔔 *Alert:*') + ..writeln('*Distance from station:* $distanceMeters meters'); + if (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A') { + buffer.writeln('*Remarks for distance:* $distanceRemarks'); } + } - final String message = buffer.toString(); + final String message = buffer.toString(); - final bool wasSent = await _telegramService.sendAlertImmediately('river_in_situ', message); - if (!wasSent) { - await _telegramService.queueMessage('river_in_situ', message); - } + // MODIFIED: Pass the appSettings list to the TelegramService methods. + final bool wasSent = await _telegramService.sendAlertImmediately('river_in_situ', message, appSettings); + if (!wasSent) { + await _telegramService.queueMessage('river_in_situ', message, appSettings); } } catch (e) { debugPrint("Failed to handle River Telegram alert: $e"); diff --git a/lib/services/river_in_situ_sampling_service.dart b/lib/services/river_in_situ_sampling_service.dart index 197ec1a..e548073 100644 --- a/lib/services/river_in_situ_sampling_service.dart +++ b/lib/services/river_in_situ_sampling_service.dart @@ -149,12 +149,12 @@ class RiverInSituSamplingService { } // --- Data Submission --- - // CHANGED: Use the river-specific data model - Future> submitData(RiverInSituSamplingData data) { - // CHANGED: Call the river-specific API service method + // MODIFIED: Method now requires the appSettings list to pass to the RiverApiService. + Future> submitData(RiverInSituSamplingData data, List>? appSettings) { return _riverApiService.submitInSituSample( formData: data.toApiFormData(), imageFiles: data.toApiImageFiles(), + appSettings: appSettings, // Added this required parameter ); } -} +} \ No newline at end of file diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 586e15f..e045ee7 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -1,99 +1,74 @@ -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:environment_monitoring_app/services/base_api_service.dart'; +import 'package:flutter/foundation.dart'; + +// No longer needs SharedPreferences or BaseApiService for its core logic. class SettingsService { - final BaseApiService _baseService = BaseApiService(); + // The service no longer manages its own state or makes API calls. + // It is now a utility for parsing the settings list managed by AuthProvider. - // Keys for SharedPreferences - static const _inSituChatIdKey = 'telegram_in_situ_chat_id'; - static const _tarballChatIdKey = 'telegram_tarball_chat_id'; - static const _riverInSituChatIdKey = 'telegram_river_in_situ_chat_id'; - static const _riverTriennialChatIdKey = 'telegram_river_triennial_chat_id'; - static const _riverInvestigativeChatIdKey = 'telegram_river_investigative_chat_id'; - static const _airManualChatIdKey = 'telegram_air_manual_chat_id'; - static const _airInvestigativeChatIdKey = 'telegram_air_investigative_chat_id'; - static const _marineInvestigativeChatIdKey = 'telegram_marine_investigative_chat_id'; + /// A private helper method to find a specific setting value from the cached list. + String _getChatId(List>? settings, String moduleName) { + if (settings == null) { + debugPrint("SettingsService: Cannot get Chat ID for '$moduleName', settings list is null."); + return ''; + } - /// Fetches settings from the server and saves them to local storage. - Future syncFromServer() async { try { - final result = await _baseService.get('settings'); + // Find the specific setting map where the module_name and setting_key match. + final setting = settings.firstWhere( + (s) => s['module_name'] == moduleName && s['setting_key'] == 'telegram_chat_id', + // If no matching setting is found, return an empty map to avoid errors. + orElse: () => {}, + ); - if (result['success'] == true && result['data'] is Map) { - final settings = result['data'] as Map; - final prefs = await SharedPreferences.getInstance(); - - // Save all chat IDs from the nested maps - await Future.wait([ - _saveChatId(prefs, _inSituChatIdKey, settings['marine_in_situ']), - _saveChatId(prefs, _tarballChatIdKey, settings['marine_tarball']), - _saveChatId(prefs, _riverInSituChatIdKey, settings['river_in_situ']), - _saveChatId(prefs, _riverTriennialChatIdKey, settings['river_triennial']), - _saveChatId(prefs, _riverInvestigativeChatIdKey, settings['river_investigative']), - _saveChatId(prefs, _airManualChatIdKey, settings['air_manual']), - _saveChatId(prefs, _airInvestigativeChatIdKey, settings['air_investigative']), - _saveChatId(prefs, _marineInvestigativeChatIdKey, settings['marine_investigative']), - ]); - - return true; + if (setting.isNotEmpty) { + return setting['setting_value']?.toString() ?? ''; } - return false; } catch (e) { - return false; + debugPrint("SettingsService: Error parsing chat ID for '$moduleName': $e"); } + + debugPrint("SettingsService: Chat ID for module '$moduleName' not found."); + return ''; } - Future _saveChatId(SharedPreferences prefs, String key, dynamic settings) async { - if (settings is Map) { - await prefs.setString(key, settings['telegram_chat_id']?.toString() ?? ''); - } + /// Gets the Chat ID for the Marine In-Situ module from the provided settings list. + String getInSituChatId(List>? settings) { + return _getChatId(settings, 'marine_in_situ'); } - /// Gets the locally stored Chat ID for the In-Situ module. - Future getInSituChatId() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString(_inSituChatIdKey) ?? ''; + /// Gets the Chat ID for the Tarball module from the provided settings list. + String getTarballChatId(List>? settings) { + return _getChatId(settings, 'marine_tarball'); } - /// Gets the locally stored Chat ID for the Tarball module. - Future getTarballChatId() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString(_tarballChatIdKey) ?? ''; + /// Gets the Chat ID for the River In-Situ module from the provided settings list. + String getRiverInSituChatId(List>? settings) { + return _getChatId(settings, 'river_in_situ'); } - /// Gets the locally stored Chat ID for the River In-Situ module. - Future getRiverInSituChatId() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString(_riverInSituChatIdKey) ?? ''; + /// Gets the Chat ID for the River Triennial module from the provided settings list. + String getRiverTriennialChatId(List>? settings) { + return _getChatId(settings, 'river_triennial'); } - /// Gets the locally stored Chat ID for the River Triennial module. - Future getRiverTriennialChatId() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString(_riverTriennialChatIdKey) ?? ''; + /// Gets the Chat ID for the River Investigative module from the provided settings list. + String getRiverInvestigativeChatId(List>? settings) { + return _getChatId(settings, 'river_investigative'); } - /// Gets the locally stored Chat ID for the River Investigative module. - Future getRiverInvestigativeChatId() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString(_riverInvestigativeChatIdKey) ?? ''; + /// Gets the Chat ID for the Air Manual module from the provided settings list. + String getAirManualChatId(List>? settings) { + return _getChatId(settings, 'air_manual'); } - /// Gets the locally stored Chat ID for the Air Manual module. - Future getAirManualChatId() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString(_airManualChatIdKey) ?? ''; + /// Gets the Chat ID for the Air Investigative module from the provided settings list. + String getAirInvestigativeChatId(List>? settings) { + return _getChatId(settings, 'air_investigative'); } - /// Gets the locally stored Chat ID for the Air Investigative module. - Future getAirInvestigativeChatId() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString(_airInvestigativeChatIdKey) ?? ''; - } - - /// Gets the locally stored Chat ID for the Marine Investigative module. - Future getMarineInvestigativeChatId() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString(_marineInvestigativeChatIdKey) ?? ''; + /// Gets the Chat ID for the Marine Investigative module from the provided settings list. + String getMarineInvestigativeChatId(List>? settings) { + return _getChatId(settings, 'marine_investigative'); } } \ No newline at end of file diff --git a/lib/services/telegram_service.dart b/lib/services/telegram_service.dart index be1e518..c8357f8 100644 --- a/lib/services/telegram_service.dart +++ b/lib/services/telegram_service.dart @@ -10,14 +10,15 @@ class TelegramService { bool _isProcessing = false; - Future _getChatIdForModule(String module) async { + // MODIFIED: This method is now synchronous and requires the appSettings list. + String _getChatIdForModule(String module, List>? appSettings) { switch (module) { case 'marine_in_situ': - return await _settingsService.getInSituChatId(); + return _settingsService.getInSituChatId(appSettings); case 'marine_tarball': - return await _settingsService.getTarballChatId(); + return _settingsService.getTarballChatId(appSettings); case 'air_manual': // ADDED THIS CASE - return await _settingsService.getAirManualChatId(); + return _settingsService.getAirManualChatId(appSettings); default: return ''; } @@ -25,9 +26,10 @@ class TelegramService { /// Tries to send an alert immediately over the network. /// Returns `true` on success, `false` on failure. - Future sendAlertImmediately(String module, String message) async { + // MODIFIED: This method now requires the appSettings list to be passed in. + Future sendAlertImmediately(String module, String message, List>? appSettings) async { debugPrint("[TelegramService] Attempting to send alert immediately for module: $module"); - String chatId = await _getChatIdForModule(module); + String chatId = _getChatIdForModule(module, appSettings); if (chatId.isEmpty) { debugPrint("[TelegramService] ❌ Cannot send immediately. Chat ID for module '$module' is not configured."); @@ -49,8 +51,9 @@ class TelegramService { } /// Saves an alert to the local database queue. (This is now the fallback) - Future queueMessage(String module, String message) async { - String chatId = await _getChatIdForModule(module); + // MODIFIED: This method now requires the appSettings list to be passed in. + Future queueMessage(String module, String message, List>? appSettings) async { + String chatId = _getChatIdForModule(module, appSettings); if (chatId.isEmpty) { debugPrint("[TelegramService] ❌ ERROR: Cannot queue alert. Chat ID for module '$module' is not configured."); @@ -71,6 +74,7 @@ class TelegramService { } /// Processes all pending alerts in the queue. + /// This method does NOT need changes because the chatId is already stored in the queue. Future processAlertQueue() async { if (_isProcessing) { debugPrint("[TelegramService] âŗ Queue is already being processed. Skipping."); @@ -112,4 +116,4 @@ class TelegramService { debugPrint("[TelegramService] âšī¸ Finished processing alert queue."); _isProcessing = false; } -} +} \ No newline at end of file