environment_monitoring_app/lib/auth_provider.dart

304 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'dart:convert';
import 'package:environment_monitoring_app/services/api_service.dart';
// --- ADDED: Import for the service that manages active server configurations ---
import 'package:environment_monitoring_app/services/server_config_service.dart';
// --- ADDED: Import for the new service that manages the retry queue ---
import 'package:environment_monitoring_app/services/retry_service.dart';
/// A comprehensive provider to manage user authentication, session state,
/// and cached master data for offline use.
class AuthProvider with ChangeNotifier {
final ApiService _apiService = ApiService();
final DatabaseHelper _dbHelper = DatabaseHelper();
// --- ADDED: Instance of the ServerConfigService to set the initial URL ---
final ServerConfigService _serverConfigService = ServerConfigService();
// --- ADDED: Instance of the RetryService to manage pending tasks ---
final RetryService _retryService = RetryService();
// --- Session & Profile State ---
String? _jwtToken;
String? _userEmail;
Map<String, dynamic>? _profileData;
bool get isLoggedIn => _jwtToken != null;
String? get userEmail => _userEmail;
Map<String, dynamic>? get profileData => _profileData;
// --- App State ---
bool _isLoading = true;
bool _isFirstLogin = true;
DateTime? _lastSyncTimestamp;
bool get isLoading => _isLoading;
bool get isFirstLogin => _isFirstLogin;
DateTime? get lastSyncTimestamp => _lastSyncTimestamp;
// --- Cached Master Data ---
List<Map<String, dynamic>>? _allUsers;
List<Map<String, dynamic>>? _tarballStations;
List<Map<String, dynamic>>? _manualStations;
List<Map<String, dynamic>>? _tarballClassifications;
List<Map<String, dynamic>>? _riverManualStations;
List<Map<String, dynamic>>? _riverTriennialStations;
List<Map<String, dynamic>>? _departments;
List<Map<String, dynamic>>? _companies;
List<Map<String, dynamic>>? _positions;
List<Map<String, dynamic>>? _airClients;
List<Map<String, dynamic>>? _airManualStations;
List<Map<String, dynamic>>? _states;
List<Map<String, dynamic>>? _appSettings;
List<Map<String, dynamic>>? _parameterLimits;
List<Map<String, dynamic>>? _apiConfigs;
List<Map<String, dynamic>>? _ftpConfigs;
// --- ADDED: State variable for the list of tasks pending manual retry ---
List<Map<String, dynamic>>? _pendingRetries;
// --- Getters for UI access ---
List<Map<String, dynamic>>? get allUsers => _allUsers;
List<Map<String, dynamic>>? get tarballStations => _tarballStations;
List<Map<String, dynamic>>? get manualStations => _manualStations;
List<Map<String, dynamic>>? get tarballClassifications => _tarballClassifications;
List<Map<String, dynamic>>? get riverManualStations => _riverManualStations;
List<Map<String, dynamic>>? get riverTriennialStations => _riverTriennialStations;
List<Map<String, dynamic>>? get departments => _departments;
List<Map<String, dynamic>>? get companies => _companies;
List<Map<String, dynamic>>? get positions => _positions;
List<Map<String, dynamic>>? get airClients => _airClients;
List<Map<String, dynamic>>? get airManualStations => _airManualStations;
List<Map<String, dynamic>>? get states => _states;
List<Map<String, dynamic>>? get appSettings => _appSettings;
List<Map<String, dynamic>>? get parameterLimits => _parameterLimits;
List<Map<String, dynamic>>? get apiConfigs => _apiConfigs;
List<Map<String, dynamic>>? get ftpConfigs => _ftpConfigs;
// --- ADDED: Getter for the list of tasks pending manual retry ---
List<Map<String, dynamic>>? get pendingRetries => _pendingRetries;
// --- SharedPreferences Keys ---
static const String tokenKey = 'jwt_token';
static const String userEmailKey = 'user_email';
static const String profileDataKey = 'user_profile_data';
static const String lastSyncTimestampKey = 'last_sync_timestamp';
static const String isFirstLoginKey = 'is_first_login';
AuthProvider() {
debugPrint('AuthProvider: Initializing...');
_loadSessionAndSyncData();
}
/// Loads the user session from storage and then triggers a data sync.
Future<void> _loadSessionAndSyncData() async {
_isLoading = true;
notifyListeners();
final prefs = await SharedPreferences.getInstance();
_jwtToken = prefs.getString(tokenKey);
_userEmail = prefs.getString(userEmailKey);
_isFirstLogin = prefs.getBool(isFirstLoginKey) ?? true;
// --- MODIFIED: Logic to insert a default URL on the first ever run ---
// Check if there is an active API configuration.
final activeApiConfig = await _serverConfigService.getActiveApiConfig();
if (activeApiConfig == null) {
// If no config is set (which will be true on the very first launch),
// we programmatically create and set a default one.
debugPrint("AuthProvider: No active API config found. Setting default bootstrap URL.");
final initialConfig = {
'api_config_id': 0,
'config_name': 'Default Server',
'api_url': 'https://dev14.pstw.com.my/v1',
};
// Save this default config as the active one.
await _serverConfigService.setActiveApiConfig(initialConfig);
}
// --- END OF MODIFICATION ---
// 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
await _loadDataFromCache();
if (_jwtToken != null) {
debugPrint('AuthProvider: Session loaded. Triggering online sync.');
// Don't await here to allow the UI to build instantly with cached data
syncAllData();
} else {
debugPrint('AuthProvider: No active session. App is in offline mode.');
}
_isLoading = false;
notifyListeners();
}
/// 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 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);
// --- ADDED: After the first successful sync, set isFirstLogin to false ---
if (_isFirstLogin) {
await setIsFirstLogin(false);
debugPrint("AuthProvider: First successful sync complete. isFirstLogin flag set to false.");
}
// --- END ---
// 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']) {
_profileData = result['data'];
// Persist the updated profile data
await _dbHelper.saveProfile(_profileData!);
final prefs = await SharedPreferences.getInstance();
await prefs.setString(profileDataKey, jsonEncode(_profileData));
notifyListeners();
}
}
/// Loads all master data from the local cache using DatabaseHelper.
Future<void> _loadDataFromCache() async {
_profileData = await _dbHelper.loadProfile();
_allUsers = await _dbHelper.loadUsers();
_tarballStations = await _dbHelper.loadTarballStations();
_manualStations = await _dbHelper.loadManualStations();
_tarballClassifications = await _dbHelper.loadTarballClassifications();
_riverManualStations = await _dbHelper.loadRiverManualStations();
_riverTriennialStations = await _dbHelper.loadRiverTriennialStations();
_departments = await _dbHelper.loadDepartments();
_companies = await _dbHelper.loadCompanies();
_positions = await _dbHelper.loadPositions();
_airClients = await _dbHelper.loadAirClients();
_airManualStations = await _dbHelper.loadAirManualStations();
_states = await _dbHelper.loadStates();
// ADDED: Load new data types from the local database
_appSettings = await _dbHelper.loadAppSettings();
_parameterLimits = await _dbHelper.loadParameterLimits();
_apiConfigs = await _dbHelper.loadApiConfigs();
_ftpConfigs = await _dbHelper.loadFtpConfigs();
// --- ADDED: Load pending retry tasks from the database ---
_pendingRetries = await _retryService.getPendingTasks();
debugPrint("AuthProvider: All master data loaded from local DB cache.");
}
// --- ADDED: A public method to allow the UI to refresh the pending tasks list ---
/// Refreshes the list of pending retry tasks from the local database.
Future<void> refreshPendingTasks() async {
_pendingRetries = await _retryService.getPendingTasks();
notifyListeners();
}
// --- Methods for UI interaction ---
/// Handles the login process, saving session data and triggering a full data sync.
Future<void> login(String token, Map<String, dynamic> profile) async {
_jwtToken = token;
_userEmail = profile['email'];
_profileData = profile;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(tokenKey, token);
await prefs.setString(userEmailKey, _userEmail!);
await prefs.setString(profileDataKey, jsonEncode(profile));
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);
}
Future<void> setProfileData(Map<String, dynamic> data) async {
_profileData = data;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(profileDataKey, jsonEncode(data));
await _dbHelper.saveProfile(data); // Also save to local DB
notifyListeners();
}
Future<void> setIsFirstLogin(bool value) async {
_isFirstLogin = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(isFirstLoginKey, value);
notifyListeners();
}
Future<void> logout() async {
debugPrint('AuthProvider: Initiating logout...');
_jwtToken = null;
_userEmail = null;
_profileData = null;
_lastSyncTimestamp = null;
_isFirstLogin = true;
_allUsers = null;
_tarballStations = null;
_manualStations = null;
_tarballClassifications = null;
_riverManualStations = null;
_riverTriennialStations = null;
_departments = null;
_companies = null;
_positions = null;
_airClients = null;
_airManualStations = null;
_states = null;
// ADDED: Clear new data on logout
_appSettings = null;
_parameterLimits = null;
_apiConfigs = null;
_ftpConfigs = null;
// --- ADDED: Clear pending retry tasks on logout ---
_pendingRetries = null;
final prefs = await SharedPreferences.getInstance();
// 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.');
notifyListeners();
}
Future<Map<String, dynamic>> resetPassword(String email) {
return _apiService.post('auth/forgot-password', {'email': email});
}
}