upgrade sync issue with delta synchronization to avoid down;lload all data every time apps got internet access
This commit is contained in:
parent
2313a4891a
commit
6d37abf141
@ -39,8 +39,9 @@ class AuthProvider with ChangeNotifier {
|
||||
List<Map<String, dynamic>>? _positions;
|
||||
List<Map<String, dynamic>>? _airClients;
|
||||
List<Map<String, dynamic>>? _airManualStations;
|
||||
// --- ADDED FOR STATE LIST ---
|
||||
List<Map<String, dynamic>>? _states;
|
||||
List<Map<String, dynamic>>? _appSettings;
|
||||
List<Map<String, dynamic>>? _parameterLimits;
|
||||
|
||||
// --- Getters for UI access ---
|
||||
List<Map<String, dynamic>>? get allUsers => _allUsers;
|
||||
@ -54,10 +55,11 @@ class AuthProvider with ChangeNotifier {
|
||||
List<Map<String, dynamic>>? get positions => _positions;
|
||||
List<Map<String, dynamic>>? get airClients => _airClients;
|
||||
List<Map<String, dynamic>>? get airManualStations => _airManualStations;
|
||||
// --- ADDED FOR STATE LIST ---
|
||||
List<Map<String, dynamic>>? get states => _states;
|
||||
List<Map<String, dynamic>>? get appSettings => _appSettings;
|
||||
List<Map<String, dynamic>>? 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<void> 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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// A dedicated method to refresh only the profile.
|
||||
Future<void> 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<void> _fetchDataFromServer() async {
|
||||
final result = await _apiService.syncAllData();
|
||||
if (result['success']) {
|
||||
final data = result['data'];
|
||||
_profileData = data['profile'];
|
||||
_allUsers = data['allUsers'] != null ? List<Map<String, dynamic>>.from(data['allUsers']) : null;
|
||||
_tarballStations = data['tarballStations'] != null ? List<Map<String, dynamic>>.from(data['tarballStations']) : null;
|
||||
_manualStations = data['manualStations'] != null ? List<Map<String, dynamic>>.from(data['manualStations']) : null;
|
||||
_tarballClassifications = data['tarballClassifications'] != null ? List<Map<String, dynamic>>.from(data['tarballClassifications']) : null;
|
||||
_riverManualStations = data['riverManualStations'] != null ? List<Map<String, dynamic>>.from(data['riverManualStations']) : null;
|
||||
_riverTriennialStations = data['riverTriennialStations'] != null ? List<Map<String, dynamic>>.from(data['riverTriennialStations']) : null;
|
||||
_departments = data['departments'] != null ? List<Map<String, dynamic>>.from(data['departments']) : null;
|
||||
_companies = data['companies'] != null ? List<Map<String, dynamic>>.from(data['companies']) : null;
|
||||
_positions = data['positions'] != null ? List<Map<String, dynamic>>.from(data['positions']) : null;
|
||||
_airClients = data['airClients'] != null ? List<Map<String, dynamic>>.from(data['airClients']) : null;
|
||||
_airManualStations = data['airManualStations'] != null ? List<Map<String, dynamic>>.from(data['airManualStations']) : null;
|
||||
|
||||
// --- ADDED FOR STATE LIST ---
|
||||
// Note: `syncAllData` in ApiService must be updated to fetch 'states'
|
||||
_states = data['states'] != null ? List<Map<String, dynamic>>.from(data['states']) : null;
|
||||
|
||||
await setLastSyncTimestamp(DateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads all master data from the local cache using DatabaseHelper.
|
||||
Future<void> _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<void> setLastSyncTimestamp(DateTime timestamp) async {
|
||||
_lastSyncTimestamp = timestamp;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(lastSyncTimestampKey, timestamp.millisecondsSinceEpoch);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> 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.');
|
||||
|
||||
@ -33,6 +33,7 @@ class _AirManualCollectionScreenState extends State<AirManualCollectionScreen> {
|
||||
.getPendingInstallations();
|
||||
}
|
||||
|
||||
// MODIFIED: This method now fetches appSettings and passes it to the service.
|
||||
Future<void> _submitCollection() async {
|
||||
if (_selectedInstallation == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@ -42,9 +43,13 @@ class _AirManualCollectionScreenState extends State<AirManualCollectionScreen> {
|
||||
}
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
// Get the required services and providers.
|
||||
final service = Provider.of<AirSamplingService>(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<AuthProvider>(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);
|
||||
|
||||
|
||||
@ -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<void> _submitInstallation() async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
// Get the required services and providers.
|
||||
final service = Provider.of<AirSamplingService>(context, listen: false);
|
||||
final result = await service.submitInstallation(_installationData);
|
||||
final authProvider = Provider.of<AuthProvider>(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),
|
||||
|
||||
@ -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<MarineManualDataStatusLog> {
|
||||
(log.reportId?.toLowerCase() ?? '').contains(query);
|
||||
}
|
||||
|
||||
// MODIFIED: This method now fetches appSettings from AuthProvider before resubmitting.
|
||||
Future<void> _resubmitData(SubmissionLogEntry log) async {
|
||||
final logKey = log.reportId ?? log.submissionDateTime.toIso8601String();
|
||||
if (mounted) {
|
||||
@ -149,7 +152,11 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
}
|
||||
|
||||
try {
|
||||
final result = await _performResubmission(log);
|
||||
// Get the appSettings from the AuthProvider to pass to the API service.
|
||||
final authProvider = Provider.of<AuthProvider>(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<MarineManualDataStatusLog> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _performResubmission(SubmissionLogEntry log) async {
|
||||
// MODIFIED: This method now requires appSettings to pass to the API service.
|
||||
Future<Map<String, dynamic>> _performResubmission(SubmissionLogEntry log, List<Map<String, dynamic>>? appSettings) async {
|
||||
final logData = log.rawData;
|
||||
|
||||
if (log.type == 'Manual Sampling') {
|
||||
@ -236,7 +244,8 @@ class _MarineManualDataStatusLogState extends State<MarineManualDataStatusLog> {
|
||||
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<MarineManualDataStatusLog> {
|
||||
}
|
||||
}
|
||||
|
||||
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}');
|
||||
|
||||
@ -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<MarineInSituSampling> {
|
||||
Future<void> _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<AuthProvider>(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;
|
||||
|
||||
|
||||
@ -86,12 +86,19 @@ class _MarineTarballSamplingState extends State<MarineTarballSampling> {
|
||||
Future<void> _getCurrentLocation() async { /* ... Location logic ... */ }
|
||||
void _calculateDistance() { /* ... Distance logic ... */ }
|
||||
|
||||
// MODIFIED: This method now fetches appSettings and passes it to the API service.
|
||||
Future<void> _submitForm() async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
// Get the appSettings list from AuthProvider.
|
||||
final authProvider = Provider.of<AuthProvider>(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;
|
||||
|
||||
@ -20,13 +20,20 @@ class _TarballSamplingStep3SummaryState extends State<TarballSamplingStep3Summar
|
||||
final LocalStorageService _localStorageService = LocalStorageService();
|
||||
bool _isLoading = false;
|
||||
|
||||
// MODIFIED: This method now fetches appSettings and passes it to the API service.
|
||||
Future<void> _submitForm() async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
// Get the appSettings list from AuthProvider.
|
||||
final authProvider = Provider.of<AuthProvider>(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;
|
||||
|
||||
@ -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<RiverDataStatusLog> {
|
||||
(log.reportId?.toLowerCase() ?? '').contains(query);
|
||||
}
|
||||
|
||||
// MODIFIED: This method now fetches appSettings from AuthProvider before resubmitting.
|
||||
Future<void> _resubmitData(SubmissionLogEntry log) async {
|
||||
final logKey = log.reportId ?? log.submissionDateTime.toIso8601String();
|
||||
if (mounted) {
|
||||
@ -151,6 +154,10 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the appSettings from the AuthProvider to pass to the API service.
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
final appSettings = authProvider.appSettings;
|
||||
|
||||
final logData = log.rawData;
|
||||
final dataToResubmit = RiverInSituSamplingData.fromJson(logData);
|
||||
final Map<String, File?> imageFiles = {};
|
||||
@ -166,9 +173,11 @@ class _RiverDataStatusLogState extends State<RiverDataStatusLog> {
|
||||
}
|
||||
}
|
||||
|
||||
// Pass the appSettings list to the submit method.
|
||||
final result = await _riverApiService.submitInSituSample(
|
||||
formData: dataToResubmit.toApiFormData(),
|
||||
imageFiles: imageFiles,
|
||||
appSettings: appSettings,
|
||||
);
|
||||
|
||||
logData['submissionStatus'] = result['status'];
|
||||
|
||||
@ -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<RiverInSituSamplingScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// MODIFIED: This method now fetches appSettings and passes it to the service.
|
||||
Future<void> _submitForm() async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
final result = await _samplingService.submitData(_data);
|
||||
// Get the appSettings list from AuthProvider.
|
||||
final authProvider = Provider.of<AuthProvider>(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;
|
||||
|
||||
|
||||
@ -12,18 +12,15 @@ class SettingsScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
// 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<SettingsScreen> {
|
||||
@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<SettingsScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _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<SettingsScreen> {
|
||||
final auth = Provider.of<AuthProvider>(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<SettingsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _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<SettingsScreen> {
|
||||
final auth = Provider.of<AuthProvider>(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<SettingsScreen> {
|
||||
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<SettingsScreen> {
|
||||
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<SettingsScreen> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -70,24 +70,28 @@ class AirSamplingService {
|
||||
return File(filePath)..writeAsBytesSync(img.encodeJpg(originalImage));
|
||||
}
|
||||
|
||||
Future<void> _handleInstallationSuccessAlert(AirInstallationData data, {required bool isDataOnly}) async {
|
||||
// MODIFIED: Method now requires the appSettings list to pass to TelegramService.
|
||||
Future<void> _handleInstallationSuccessAlert(AirInstallationData data, List<Map<String, dynamic>>? 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<void> _handleCollectionSuccessAlert(AirCollectionData data, AirInstallationData installationData, {required bool isDataOnly}) async {
|
||||
// MODIFIED: Method now requires the appSettings list to pass to TelegramService.
|
||||
Future<void> _handleCollectionSuccessAlert(AirCollectionData data, AirInstallationData installationData, List<Map<String, dynamic>>? 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<Map<String, dynamic>> submitInstallation(AirInstallationData data) async {
|
||||
// MODIFIED: Method now requires the appSettings list to pass down the call stack.
|
||||
Future<Map<String, dynamic>> submitInstallation(AirInstallationData data, List<Map<String, dynamic>>? appSettings) async {
|
||||
// --- OFFLINE-FIRST HELPER ---
|
||||
Future<Map<String, dynamic>> 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<Map<String, dynamic>> _uploadInstallationImagesAndUpdate(AirInstallationData data) async {
|
||||
// MODIFIED: Method now requires the appSettings list to pass to the alert handler.
|
||||
Future<Map<String, dynamic>> _uploadInstallationImagesAndUpdate(AirInstallationData data, List<Map<String, dynamic>>? 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<Map<String, dynamic>> submitCollection(AirCollectionData data, AirInstallationData installationData) async {
|
||||
// MODIFIED: Method now requires the appSettings list to pass down the call stack.
|
||||
Future<Map<String, dynamic>> submitCollection(AirCollectionData data, AirInstallationData installationData, List<Map<String, dynamic>>? appSettings) async {
|
||||
// --- OFFLINE-FIRST HELPER (CORRECTED) ---
|
||||
Future<Map<String, dynamic>> 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<Map<String, dynamic>> _uploadCollectionImagesAndUpdate(AirCollectionData data, AirInstallationData installationData) async {
|
||||
// MODIFIED: Method now requires the appSettings list to pass to the alert handler.
|
||||
Future<Map<String, dynamic>> _uploadCollectionImagesAndUpdate(AirCollectionData data, AirInstallationData installationData, List<Map<String, dynamic>>? appSettings) async {
|
||||
// --- OFFLINE-FIRST HELPER (CORRECTED) ---
|
||||
Future<Map<String, dynamic>> 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.',
|
||||
|
||||
@ -33,7 +33,7 @@ class ApiService {
|
||||
air = AirApiService(_baseService);
|
||||
}
|
||||
|
||||
// --- Core API Methods ---
|
||||
// --- Core API Methods (Unchanged) ---
|
||||
|
||||
Future<Map<String, dynamic>> 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<Map<String, dynamic>> syncAllData() async {
|
||||
debugPrint('ApiService: Starting full data sync from server...');
|
||||
/// Helper method to make a delta-sync API call.
|
||||
Future<Map<String, dynamic>> _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<Map<String, dynamic>> 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<String, dynamic> 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<Map<String, dynamic>>.from(syncedData['allUsers']));
|
||||
if (syncedData['tarballStations'] != null) await _dbHelper.saveTarballStations(List<Map<String, dynamic>>.from(syncedData['tarballStations']));
|
||||
if (syncedData['manualStations'] != null) await _dbHelper.saveManualStations(List<Map<String, dynamic>>.from(syncedData['manualStations']));
|
||||
if (syncedData['tarballClassifications'] != null) await _dbHelper.saveTarballClassifications(List<Map<String, dynamic>>.from(syncedData['tarballClassifications']));
|
||||
if (syncedData['riverManualStations'] != null) await _dbHelper.saveRiverManualStations(List<Map<String, dynamic>>.from(syncedData['riverManualStations']));
|
||||
if (syncedData['riverTriennialStations'] != null) await _dbHelper.saveRiverTriennialStations(List<Map<String, dynamic>>.from(syncedData['riverTriennialStations']));
|
||||
if (syncedData['departments'] != null) await _dbHelper.saveDepartments(List<Map<String, dynamic>>.from(syncedData['departments']));
|
||||
if (syncedData['companies'] != null) await _dbHelper.saveCompanies(List<Map<String, dynamic>>.from(syncedData['companies']));
|
||||
if (syncedData['positions'] != null) await _dbHelper.savePositions(List<Map<String, dynamic>>.from(syncedData['positions']));
|
||||
if (syncedData['airManualStations'] != null) await _dbHelper.saveAirManualStations(List<Map<String, dynamic>>.from(syncedData['airManualStations']));
|
||||
if (syncedData['airClients'] != null) await _dbHelper.saveAirClients(List<Map<String, dynamic>>.from(syncedData['airClients']));
|
||||
if (syncedData['states'] != null) await _dbHelper.saveStates(List<Map<String, dynamic>>.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<Map<String, dynamic>>.from(result['data']['updated'] ?? []);
|
||||
final deleted = List<dynamic>.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<Map<String, dynamic>> getManualStations() => _baseService.get('air/manual-stations');
|
||||
Future<Map<String, dynamic>> getClients() => _baseService.get('air/clients');
|
||||
|
||||
// NEW: Added dedicated method for uploading installation images
|
||||
Future<Map<String, dynamic>> uploadInstallationImages({
|
||||
required String airManId,
|
||||
required Map<String, File> files,
|
||||
@ -205,13 +213,11 @@ class AirApiService {
|
||||
);
|
||||
}
|
||||
|
||||
// NECESSARY FIX: Added dedicated method for uploading collection images
|
||||
Future<Map<String, dynamic>> uploadCollectionImages({
|
||||
required String airManId,
|
||||
required Map<String, File> 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<Database> 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<void> _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)');
|
||||
}
|
||||
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<void> _saveData(String table, String idKey, List<Map<String, dynamic>> data) async {
|
||||
/// Performs an "upsert": inserts new records or replaces existing ones.
|
||||
Future<void> _upsertData(String table, String idKeyName, List<Map<String, dynamic>> data, String jsonKeyName) async {
|
||||
if (data.isEmpty) return;
|
||||
final db = await database;
|
||||
await db.delete(table);
|
||||
final batch = db.batch();
|
||||
for (var item in data) {
|
||||
await db.insert(table, {'${idKey}_id': item['${idKey}_id'], '${idKey}_json': jsonEncode(item)}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
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");
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>?> _loadData(String table, String idKey) async {
|
||||
/// Deletes a list of records from a table by their primary keys.
|
||||
Future<void> _deleteData(String table, String idKeyName, List<dynamic> 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<List<Map<String, dynamic>>?> _loadData(String table, String jsonKey) async {
|
||||
final db = await database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(table);
|
||||
if (maps.isNotEmpty) {
|
||||
return maps.map((map) => jsonDecode(map['${idKey}_json']) as Map<String, dynamic>).toList();
|
||||
return maps.map((map) => jsonDecode(map['${jsonKey}_json']) as Map<String, dynamic>).toList();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -349,39 +388,61 @@ class DatabaseHelper {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> saveUsers(List<Map<String, dynamic>> users) => _saveData(_usersTable, 'user', users);
|
||||
// --- Upsert/Delete/Load methods for all data types ---
|
||||
|
||||
Future<void> upsertUsers(List<Map<String, dynamic>> data) => _upsertData(_usersTable, 'user_id', data, 'user');
|
||||
Future<void> deleteUsers(List<dynamic> ids) => _deleteData(_usersTable, 'user_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadUsers() => _loadData(_usersTable, 'user');
|
||||
|
||||
Future<void> saveTarballStations(List<Map<String, dynamic>> stations) => _saveData(_tarballStationsTable, 'station', stations);
|
||||
Future<void> upsertTarballStations(List<Map<String, dynamic>> data) => _upsertData(_tarballStationsTable, 'station_id', data, 'station');
|
||||
Future<void> deleteTarballStations(List<dynamic> ids) => _deleteData(_tarballStationsTable, 'station_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadTarballStations() => _loadData(_tarballStationsTable, 'station');
|
||||
|
||||
Future<void> saveManualStations(List<Map<String, dynamic>> stations) => _saveData(_manualStationsTable, 'station', stations);
|
||||
Future<void> upsertManualStations(List<Map<String, dynamic>> data) => _upsertData(_manualStationsTable, 'station_id', data, 'station');
|
||||
Future<void> deleteManualStations(List<dynamic> ids) => _deleteData(_manualStationsTable, 'station_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadManualStations() => _loadData(_manualStationsTable, 'station');
|
||||
|
||||
Future<void> saveRiverManualStations(List<Map<String, dynamic>> stations) => _saveData(_riverManualStationsTable, 'station', stations);
|
||||
Future<void> upsertRiverManualStations(List<Map<String, dynamic>> data) => _upsertData(_riverManualStationsTable, 'station_id', data, 'station');
|
||||
Future<void> deleteRiverManualStations(List<dynamic> ids) => _deleteData(_riverManualStationsTable, 'station_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadRiverManualStations() => _loadData(_riverManualStationsTable, 'station');
|
||||
|
||||
Future<void> saveRiverTriennialStations(List<Map<String, dynamic>> stations) => _saveData(_riverTriennialStationsTable, 'station', stations);
|
||||
Future<void> upsertRiverTriennialStations(List<Map<String, dynamic>> data) => _upsertData(_riverTriennialStationsTable, 'station_id', data, 'station');
|
||||
Future<void> deleteRiverTriennialStations(List<dynamic> ids) => _deleteData(_riverTriennialStationsTable, 'station_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadRiverTriennialStations() => _loadData(_riverTriennialStationsTable, 'station');
|
||||
|
||||
Future<void> saveTarballClassifications(List<Map<String, dynamic>> data) => _saveData(_tarballClassificationsTable, 'classification', data);
|
||||
Future<void> upsertTarballClassifications(List<Map<String, dynamic>> data) => _upsertData(_tarballClassificationsTable, 'classification_id', data, 'classification');
|
||||
Future<void> deleteTarballClassifications(List<dynamic> ids) => _deleteData(_tarballClassificationsTable, 'classification_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadTarballClassifications() => _loadData(_tarballClassificationsTable, 'classification');
|
||||
|
||||
Future<void> saveDepartments(List<Map<String, dynamic>> data) => _saveData(_departmentsTable, 'department', data);
|
||||
Future<void> upsertDepartments(List<Map<String, dynamic>> data) => _upsertData(_departmentsTable, 'department_id', data, 'department');
|
||||
Future<void> deleteDepartments(List<dynamic> ids) => _deleteData(_departmentsTable, 'department_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadDepartments() => _loadData(_departmentsTable, 'department');
|
||||
|
||||
Future<void> saveCompanies(List<Map<String, dynamic>> data) => _saveData(_companiesTable, 'company', data);
|
||||
Future<void> upsertCompanies(List<Map<String, dynamic>> data) => _upsertData(_companiesTable, 'company_id', data, 'company');
|
||||
Future<void> deleteCompanies(List<dynamic> ids) => _deleteData(_companiesTable, 'company_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadCompanies() => _loadData(_companiesTable, 'company');
|
||||
|
||||
Future<void> savePositions(List<Map<String, dynamic>> data) => _saveData(_positionsTable, 'position', data);
|
||||
Future<void> upsertPositions(List<Map<String, dynamic>> data) => _upsertData(_positionsTable, 'position_id', data, 'position');
|
||||
Future<void> deletePositions(List<dynamic> ids) => _deleteData(_positionsTable, 'position_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadPositions() => _loadData(_positionsTable, 'position');
|
||||
|
||||
Future<void> saveAirManualStations(List<Map<String, dynamic>> stations) => _saveData(_airManualStationsTable, 'station', stations);
|
||||
Future<void> upsertAirManualStations(List<Map<String, dynamic>> data) => _upsertData(_airManualStationsTable, 'station_id', data, 'station');
|
||||
Future<void> deleteAirManualStations(List<dynamic> ids) => _deleteData(_airManualStationsTable, 'station_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadAirManualStations() => _loadData(_airManualStationsTable, 'station');
|
||||
|
||||
Future<void> saveAirClients(List<Map<String, dynamic>> clients) => _saveData(_airClientsTable, 'client', clients);
|
||||
Future<void> upsertAirClients(List<Map<String, dynamic>> data) => _upsertData(_airClientsTable, 'client_id', data, 'client');
|
||||
Future<void> deleteAirClients(List<dynamic> ids) => _deleteData(_airClientsTable, 'client_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadAirClients() => _loadData(_airClientsTable, 'client');
|
||||
|
||||
Future<void> saveStates(List<Map<String, dynamic>> states) => _saveData(_statesTable, 'state', states);
|
||||
Future<void> upsertStates(List<Map<String, dynamic>> data) => _upsertData(_statesTable, 'state_id', data, 'state');
|
||||
Future<void> deleteStates(List<dynamic> ids) => _deleteData(_statesTable, 'state_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadStates() => _loadData(_statesTable, 'state');
|
||||
|
||||
Future<void> upsertAppSettings(List<Map<String, dynamic>> data) => _upsertData(_appSettingsTable, 'setting_id', data, 'setting');
|
||||
Future<void> deleteAppSettings(List<dynamic> ids) => _deleteData(_appSettingsTable, 'setting_id', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadAppSettings() => _loadData(_appSettingsTable, 'setting');
|
||||
|
||||
Future<void> upsertParameterLimits(List<Map<String, dynamic>> data) => _upsertData(_parameterLimitsTable, 'param_autoid', data, 'limit');
|
||||
Future<void> deleteParameterLimits(List<dynamic> ids) => _deleteData(_parameterLimitsTable, 'param_autoid', ids);
|
||||
Future<List<Map<String, dynamic>>?> loadParameterLimits() => _loadData(_parameterLimitsTable, 'limit');
|
||||
}
|
||||
@ -142,11 +142,13 @@ class InSituSamplingService {
|
||||
}
|
||||
|
||||
// --- Data Submission ---
|
||||
Future<Map<String, dynamic>> submitData(InSituSamplingData data) {
|
||||
// MODIFIED: Method now requires the appSettings list to pass to the MarineApiService.
|
||||
Future<Map<String, dynamic>> submitData(InSituSamplingData data, List<Map<String, dynamic>>? appSettings) {
|
||||
return _marineApiService.submitInSituSample(
|
||||
formData: data.toApiFormData(),
|
||||
imageFiles: data.toApiImageFiles(),
|
||||
inSituData: data, // Added this required parameter
|
||||
inSituData: data,
|
||||
appSettings: appSettings, // Added this required parameter
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<Map<String, dynamic>> 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<Map<String, dynamic>> submitTarballSample({
|
||||
required Map<String, String> formData,
|
||||
required Map<String, File?> imageFiles,
|
||||
required List<Map<String, dynamic>>? 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<Map<String, dynamic>> submitInSituSample({
|
||||
required Map<String, String> formData,
|
||||
required Map<String, File?> imageFiles,
|
||||
required InSituSamplingData inSituData,
|
||||
required List<Map<String, dynamic>>? 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<void> _handleTarballSuccessAlert(Map<String, String> formData, {required bool isDataOnly}) async {
|
||||
// MODIFIED: Method now requires appSettings and calls the updated TelegramService.
|
||||
Future<void> _handleTarballSuccessAlert(Map<String, String> formData, List<Map<String, dynamic>>? 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);
|
||||
final bool wasSent = await _telegramService.sendAlertImmediately('marine_tarball', message, appSettings);
|
||||
if (!wasSent) {
|
||||
await _telegramService.queueMessage('marine_tarball', message);
|
||||
}
|
||||
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<void> _handleInSituSuccessAlert(InSituSamplingData data, {required bool isDataOnly}) async {
|
||||
// MODIFIED: Method now requires appSettings and calls the updated TelegramService.
|
||||
Future<void> _handleInSituSuccessAlert(InSituSamplingData data, List<Map<String, dynamic>>? 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);
|
||||
final bool wasSent = await _telegramService.sendAlertImmediately('marine_in_situ', message, appSettings);
|
||||
if (!wasSent) {
|
||||
await _telegramService.queueMessage('marine_in_situ', message);
|
||||
}
|
||||
await _telegramService.queueMessage('marine_in_situ', message, appSettings);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Failed to handle In-Situ Telegram alert: $e");
|
||||
|
||||
@ -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<Map<String, dynamic>> 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<Map<String, dynamic>> submitInSituSample({
|
||||
required Map<String, String> formData,
|
||||
required Map<String, File?> imageFiles,
|
||||
required List<Map<String, dynamic>>? 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,10 +92,9 @@ class RiverApiService {
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _handleInSituSuccessAlert(Map<String, String> formData, {required bool isDataOnly}) async {
|
||||
// MODIFIED: Method now requires appSettings and calls the updated TelegramService.
|
||||
Future<void> _handleInSituSuccessAlert(Map<String, String> formData, List<Map<String, dynamic>>? 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';
|
||||
@ -123,10 +126,10 @@ class RiverApiService {
|
||||
|
||||
final String message = buffer.toString();
|
||||
|
||||
final bool wasSent = await _telegramService.sendAlertImmediately('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);
|
||||
}
|
||||
await _telegramService.queueMessage('river_in_situ', message, appSettings);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Failed to handle River Telegram alert: $e");
|
||||
|
||||
@ -149,12 +149,12 @@ class RiverInSituSamplingService {
|
||||
}
|
||||
|
||||
// --- Data Submission ---
|
||||
// CHANGED: Use the river-specific data model
|
||||
Future<Map<String, dynamic>> submitData(RiverInSituSamplingData data) {
|
||||
// CHANGED: Call the river-specific API service method
|
||||
// MODIFIED: Method now requires the appSettings list to pass to the RiverApiService.
|
||||
Future<Map<String, dynamic>> submitData(RiverInSituSamplingData data, List<Map<String, dynamic>>? appSettings) {
|
||||
return _riverApiService.submitInSituSample(
|
||||
formData: data.toApiFormData(),
|
||||
imageFiles: data.toApiImageFiles(),
|
||||
appSettings: appSettings, // Added this required parameter
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<Map<String, dynamic>>? 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<bool> 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<String, dynamic>;
|
||||
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");
|
||||
}
|
||||
|
||||
Future<void> _saveChatId(SharedPreferences prefs, String key, dynamic settings) async {
|
||||
if (settings is Map<String, dynamic>) {
|
||||
await prefs.setString(key, settings['telegram_chat_id']?.toString() ?? '');
|
||||
}
|
||||
debugPrint("SettingsService: Chat ID for module '$moduleName' not found.");
|
||||
return '';
|
||||
}
|
||||
|
||||
/// Gets the locally stored Chat ID for the In-Situ module.
|
||||
Future<String> getInSituChatId() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_inSituChatIdKey) ?? '';
|
||||
/// Gets the Chat ID for the Marine In-Situ module from the provided settings list.
|
||||
String getInSituChatId(List<Map<String, dynamic>>? settings) {
|
||||
return _getChatId(settings, 'marine_in_situ');
|
||||
}
|
||||
|
||||
/// Gets the locally stored Chat ID for the Tarball module.
|
||||
Future<String> getTarballChatId() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_tarballChatIdKey) ?? '';
|
||||
/// Gets the Chat ID for the Tarball module from the provided settings list.
|
||||
String getTarballChatId(List<Map<String, dynamic>>? settings) {
|
||||
return _getChatId(settings, 'marine_tarball');
|
||||
}
|
||||
|
||||
/// Gets the locally stored Chat ID for the River In-Situ module.
|
||||
Future<String> getRiverInSituChatId() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_riverInSituChatIdKey) ?? '';
|
||||
/// Gets the Chat ID for the River In-Situ module from the provided settings list.
|
||||
String getRiverInSituChatId(List<Map<String, dynamic>>? settings) {
|
||||
return _getChatId(settings, 'river_in_situ');
|
||||
}
|
||||
|
||||
/// Gets the locally stored Chat ID for the River Triennial module.
|
||||
Future<String> getRiverTriennialChatId() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_riverTriennialChatIdKey) ?? '';
|
||||
/// Gets the Chat ID for the River Triennial module from the provided settings list.
|
||||
String getRiverTriennialChatId(List<Map<String, dynamic>>? settings) {
|
||||
return _getChatId(settings, 'river_triennial');
|
||||
}
|
||||
|
||||
/// Gets the locally stored Chat ID for the River Investigative module.
|
||||
Future<String> getRiverInvestigativeChatId() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_riverInvestigativeChatIdKey) ?? '';
|
||||
/// Gets the Chat ID for the River Investigative module from the provided settings list.
|
||||
String getRiverInvestigativeChatId(List<Map<String, dynamic>>? settings) {
|
||||
return _getChatId(settings, 'river_investigative');
|
||||
}
|
||||
|
||||
/// Gets the locally stored Chat ID for the Air Manual module.
|
||||
Future<String> getAirManualChatId() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_airManualChatIdKey) ?? '';
|
||||
/// Gets the Chat ID for the Air Manual module from the provided settings list.
|
||||
String getAirManualChatId(List<Map<String, dynamic>>? settings) {
|
||||
return _getChatId(settings, 'air_manual');
|
||||
}
|
||||
|
||||
/// Gets the locally stored Chat ID for the Air Investigative module.
|
||||
Future<String> getAirInvestigativeChatId() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_airInvestigativeChatIdKey) ?? '';
|
||||
/// Gets the Chat ID for the Air Investigative module from the provided settings list.
|
||||
String getAirInvestigativeChatId(List<Map<String, dynamic>>? settings) {
|
||||
return _getChatId(settings, 'air_investigative');
|
||||
}
|
||||
|
||||
/// Gets the locally stored Chat ID for the Marine Investigative module.
|
||||
Future<String> 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<Map<String, dynamic>>? settings) {
|
||||
return _getChatId(settings, 'marine_investigative');
|
||||
}
|
||||
}
|
||||
@ -10,14 +10,15 @@ class TelegramService {
|
||||
|
||||
bool _isProcessing = false;
|
||||
|
||||
Future<String> _getChatIdForModule(String module) async {
|
||||
// MODIFIED: This method is now synchronous and requires the appSettings list.
|
||||
String _getChatIdForModule(String module, List<Map<String, dynamic>>? 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<bool> sendAlertImmediately(String module, String message) async {
|
||||
// MODIFIED: This method now requires the appSettings list to be passed in.
|
||||
Future<bool> sendAlertImmediately(String module, String message, List<Map<String, dynamic>>? 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<void> queueMessage(String module, String message) async {
|
||||
String chatId = await _getChatIdForModule(module);
|
||||
// MODIFIED: This method now requires the appSettings list to be passed in.
|
||||
Future<void> queueMessage(String module, String message, List<Map<String, dynamic>>? 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<void> processAlertQueue() async {
|
||||
if (_isProcessing) {
|
||||
debugPrint("[TelegramService] ⏳ Queue is already being processed. Skipping.");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user