TKK_E32221098/lib/provider/rtdb_handler.dart

722 lines
32 KiB
Dart

import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:seedina/services/auth_service.dart'; //
class HandlingProvider extends ChangeNotifier {
// --- STATE AKTIF (Untuk Overview & Data Tersimpan) ---
Map<String, dynamic> _activeParameters = {};
String _activeSelectedPlant = '';
// Nilai sensor aktual
int humiLing = 0;
int tdsAir = 0;
double ecAir = 0.0;
double tinggiAir = 0.0;
double suhuAir = 0.0;
double suhuLing = 0.0;
// Nilai ideal yang diturunkan dari _activeParameters (untuk Overview)
String namaTanaman = 'Pilih Tanaman';
String namaLatin = '...';
String gambar = 'assets/myicon/unknown.png';
double idealWaktuSiram = 0.0;
double idealJedaSiram = 0.0;
double idealSuhu = 0.0;
double idealNutrisi = 0.0;
double idealEC = 0.0;
// --- STATE DRAFT (Untuk Halaman EditPlants) ---
Map<String, dynamic> _draftParametersForEditing = {};
String _draftSelectedPlantForEditing = '';
// --- State Lainnya ---
String? _userSeedkey;
bool _isInitialized = false;
bool _isLoading = true;
bool _isSeedKeyReady = false;
double? onDemandPhValue;
bool isPhMeasuring = false;
String? phMeasurementError;
StreamSubscription<DatabaseEvent>? _monitoringSubscription;
StreamSubscription<DatabaseEvent>? _parameterSubscription;
StreamSubscription<DatabaseEvent>? _onDemandPhSubscription;
StreamSubscription<User?>? _authSubscription;
final Map<String, Map<String, dynamic>> parameters = {
'Selada Romaine': {
'title': 'Selada Romaine',
'latin': 'Lactuca sativa L. var. longifolia',
'thumbnail': 'assets/plants/lettuce.png', //
'jeda_siram': 25,
'waktu_siram': 3,
'min_suhuair': 18.0,
'max_suhuair': 24.0,
'min_suhuling': 15.0,
'max_suhuling': 25.0,
'min_tdsair': 560,
'max_tdsair': 840,
'min_humiling': 50,
'max_humiling': 70,
},
'Bayam': {
'title': 'Bayam',
'latin': 'Amaranthus sp.',
'thumbnail': 'assets/plants/spinach.png', //
'jeda_siram': 30,
'waktu_siram': 2,
'min_suhuair': 18.0,
'max_suhuair': 28.0,
'min_suhuling': 18.0,
'max_suhuling': 30.0,
'min_tdsair': 1260,
'max_tdsair': 1610,
'min_humiling': 60,
'max_humiling': 80,
},
'Kangkung': {
'title': 'Kangkung',
'latin': 'Ipomoea aquatica',
'thumbnail': 'assets/plants/water-spinach.png', //
'jeda_siram': 10,
'waktu_siram': 1,
'min_suhuair': 20.0,
'max_suhuair': 30.0,
'min_suhuling': 20.0,
'max_suhuling': 30.0,
'min_tdsair': 1050,
'max_tdsair': 1400,
'min_humiling': 70,
'max_humiling': 90,
},
'Kustom': {
'title': 'Tanaman Lain',
'latin': 'Parameter Kustom',
'thumbnail': 'assets/myicon/unknown.png', //
'jeda_siram': 30, // Nilai default jika tidak ada dari RTDB
'waktu_siram': 5, // Nilai default jika tidak ada dari RTDB
'min_suhuair': 15.0,
'max_suhuair': 35.0,
'min_suhuling': 15.0,
'max_suhuling': 35.0,
'min_tdsair': 350,
'max_tdsair': 2100,
'min_humiling': 30,
'max_humiling': 90,
}
};
String get activeSelectedPlant => _activeSelectedPlant;
Map<String, dynamic> get activeParametersReadonly => Map.unmodifiable(_activeParameters);
String get draftSelectedPlantForEditing => _draftSelectedPlantForEditing;
Map<String, dynamic> get draftParametersForEditingReadonly => Map.unmodifiable(_draftParametersForEditing);
Map<String, String> get draftPlantInfoForEditingPage {
final paramsToUse = _draftParametersForEditing.isNotEmpty ? _draftParametersForEditing : parameters['Kustom']!;
return {
'title': paramsToUse['title'] as String? ?? _draftSelectedPlantForEditing,
'latin': paramsToUse['latin'] as String? ?? '...',
'thumbnail': paramsToUse['thumbnail'] as String? ?? 'assets/myicon/unknown.png',
};
}
double get idealWaktuSiramForEditingPage => _parseToDouble(_draftParametersForEditing['waktu_siram']);
double get idealJedaSiramForEditingPage => _parseToDouble(_draftParametersForEditing['jeda_siram']);
double get idealSuhuForEditingPage {
if (_draftParametersForEditing.isEmpty) return 0.0;
return ((_parseToDouble(_draftParametersForEditing['min_suhuair']) + _parseToDouble(_draftParametersForEditing['max_suhuair'])) / 2);
}
double get idealNutrisiForEditingPage {
if (_draftParametersForEditing.isEmpty) return 0.0;
return ((_parseToDouble(_draftParametersForEditing['min_tdsair']) + _parseToDouble(_draftParametersForEditing['max_tdsair'])) / 2);
}
double get idealECForEditingPage {
if (_draftParametersForEditing.isEmpty) return 0.0;
double minECVal = (_parseToDouble(_draftParametersForEditing['min_tdsair']) / 700.0);
double maxECVal = (_parseToDouble(_draftParametersForEditing['max_tdsair']) / 700.0);
return (minECVal + maxECVal) / 2;
}
bool get isInitialized => _isInitialized;
bool get isLoading => _isLoading;
String? get currentUserSeedKey => _userSeedkey;
bool get isSeedKeyReady => _isSeedKeyReady;
DatabaseReference? get _baseRef {
if (!_isSeedKeyReady || _userSeedkey == null || _userSeedkey!.isEmpty) return null;
return FirebaseDatabase.instance.ref().child(_userSeedkey!);
}
DatabaseReference? get _monitoringDataRef => _baseRef?.child('monitoring');
DatabaseReference? get _activeParameterDataRef => _baseRef?.child('parameter');
DatabaseReference? get _phRequestRef => _baseRef?.child('commands/ph_request');
DatabaseReference? get _phResultRef => _baseRef?.child('ph_ondemand');
HandlingProvider() {
_authSubscription = AuthService.authStateChanges.listen((user) { //
final currentAuthUid = user?.uid;
if (currentAuthUid == null) {
_resetAllStates();
if (hasListeners) notifyListeners();
} else {
AuthService.getUserDoc(currentAuthUid).then((userDoc) { //
String? firestoreSeedKey;
String? firestoreSelectedPlant;
if (userDoc != null && userDoc.exists) {
final data = userDoc.data() as Map<String, dynamic>?;
firestoreSeedKey = data?['seedKey'];
firestoreSelectedPlant = data?['selectedPlant'];
}
bool seedKeyChanged = _userSeedkey != firestoreSeedKey;
_userSeedkey = firestoreSeedKey;
_isSeedKeyReady = _userSeedkey != null && _userSeedkey!.isNotEmpty;
if (!_isInitialized || seedKeyChanged || _activeSelectedPlant.isEmpty) {
_initializeProviderStates(persistedSelectedPlant: firestoreSelectedPlant);
} else if (_isLoading) {
_isLoading = false;
if (hasListeners) notifyListeners();
}
}).catchError((e) {
if (kDebugMode) print("Error fetching user doc: $e");
if (!_isInitialized || _userSeedkey == null) {
_initializeProviderStates();
}
});
}
});
if (AuthService.currentUser == null && !_isInitialized) { //
_initializeProviderStates();
}
}
void _resetAllStates() {
_activeParameters = {};
_activeSelectedPlant = '';
_draftParametersForEditing = {};
_draftSelectedPlantForEditing = '';
humiLing = 0; tdsAir = 0; ecAir = 0.0; tinggiAir = 0.0; suhuAir = 0.0; suhuLing = 0.0;
_updateDisplayParametersFromActiveData();
// _userSeedkey & _isSeedKeyReady diurus oleh listener auth
_isInitialized = true;
_isLoading = false;
onDemandPhValue = null; isPhMeasuring = false; phMeasurementError = null;
_cancelSubscriptions();
if (kDebugMode) print("[HandlingProvider] All states reset.");
}
void _cancelSubscriptions() {
_monitoringSubscription?.cancel(); _monitoringSubscription = null;
_parameterSubscription?.cancel(); _parameterSubscription = null;
_onDemandPhSubscription?.cancel(); _onDemandPhSubscription = null;
}
Future<void> _initializeProviderStates({String? persistedSelectedPlant}) async {
if (!_isLoading) {
_isLoading = true;
if (hasListeners) notifyListeners();
}
_isInitialized = false;
_cancelSubscriptions();
final currentUser = AuthService.currentUser; //
if (currentUser == null || !_isSeedKeyReady || _activeParameterDataRef == null) {
_activeSelectedPlant = persistedSelectedPlant ?? await _getPlantFromPrefsOrFallback();
_activeParameters = Map.from(parameters[_activeSelectedPlant] ?? parameters["Kustom"]!);
_updateDisplayParametersFromActiveData();
_initializeDraftParametersFromActive(forceOverwrite: true);
_isLoading = false;
_isInitialized = true;
if (kDebugMode) print("[HandlingProvider] Initialized (no user/seedkey/ref): ActivePlant='$_activeSelectedPlant'");
if (hasListeners) notifyListeners();
return;
}
// Prioritas selectedPlant: Firestore (dari auth listener) > SharedPreferences > Default
_activeSelectedPlant = persistedSelectedPlant ?? await _getPlantFromPrefsOrFallback();
if (kDebugMode) print("[HandlingProvider] Initializing with plant: $_activeSelectedPlant (from persisted/prefs)");
await _loadAndSetupFirebaseData();
_isInitialized = true; // isLoading dihandle di _loadAndSetupFirebaseData
}
Future<String> _getPlantFromPrefsOrFallback() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? prefPlant = prefs.getString('selectedPlant');
if (prefPlant != null && prefPlant.isNotEmpty && parameters.containsKey(prefPlant)) {
return prefPlant;
}
return parameters.keys.firstWhere((k) => k != "Kustom", orElse: () => "Kustom");
}
Future<void> _loadAndSetupFirebaseData() async {
bool initialMonitoringLoaded = false;
bool initialParameterLoaded = false;
void checkInitialLoadComplete() {
if (initialMonitoringLoaded && initialParameterLoaded) {
_initializeDraftParametersFromActive(forceOverwrite: true);
if (_isLoading) {
_isLoading = false;
if (hasListeners) notifyListeners();
if (kDebugMode) print("[HandlingProvider] Firebase initial load complete. ActivePlant='$_activeSelectedPlant'");
}
}
}
try {
DatabaseEvent initialParameterEvent = await _activeParameterDataRef!.once().timeout(const Duration(seconds: 7));
_handleActiveParameterData(initialParameterEvent.snapshot, isInitialLoad: true);
} catch (e) {
if (kDebugMode) print("[HandlingProvider] Error loading initial active parameters from RTDB: $e. Using local for '$_activeSelectedPlant'.");
_activeParameters = Map.from(parameters[_activeSelectedPlant] ?? parameters["Kustom"]!);
_updateDisplayParametersFromActiveData();
} finally {
initialParameterLoaded = true;
checkInitialLoadComplete();
}
try {
DatabaseEvent initialMonitoringEvent = await _monitoringDataRef!.once().timeout(const Duration(seconds: 7));
_handleMonitoringData(initialMonitoringEvent);
} catch (e) {
_resetSensorValues();
if (kDebugMode) print("[HandlingProvider] Error loading initial monitoring data: $e.");
} finally {
initialMonitoringLoaded = true;
checkInitialLoadComplete();
}
_parameterSubscription = _activeParameterDataRef!.onValue.listen(
(event) => _handleActiveParameterData(event.snapshot),
onError: (e) => _handleListenerError("ActiveParameter", e));
_monitoringSubscription = _monitoringDataRef!.onValue.listen(_handleMonitoringData, onError: (e) => _handleListenerError("Monitoring", e));
if (_phResultRef != null) {
_onDemandPhSubscription = _phResultRef!.onValue.listen(_handlePhResult, onError: (e) => _handleListenerError("OnDemandPHResult", e));
}
}
void _resetSensorValues() {
humiLing = 0; tdsAir = 0; ecAir = 0.0; tinggiAir = 0.0; suhuAir = 0.0; suhuLing = 0.0;
}
void _handleActiveParameterData(DataSnapshot snapshot, {bool isInitialLoad = false}) {
final dynamic data = snapshot.value;
Map<String, dynamic> paramsFromRTDB;
String determinedPlantType = _activeSelectedPlant; // Mulai dengan apa yang sudah ada (dari Prefs/Firestore)
if (data != null && data is Map && data.isNotEmpty) {
paramsFromRTDB = Map<String, dynamic>.from(data);
final String? rtdbTitle = paramsFromRTDB['title'] as String?;
if (rtdbTitle != null && rtdbTitle.isNotEmpty) {
if (parameters.containsKey(rtdbTitle)) { // Judul adalah nama preset
determinedPlantType = rtdbTitle;
// Lengkapi paramsFromRTDB dengan default preset jika ada field yang hilang
Map<String, dynamic> presetDefaults = Map.from(parameters[determinedPlantType]!);
presetDefaults.addAll(paramsFromRTDB); // Timpa default dengan yang dari RTDB
paramsFromRTDB = presetDefaults;
} else { // Judul bukan preset, ini pasti Kustom
determinedPlantType = "Kustom";
// Pastikan field Kustom default ada jika tidak ada di RTDB, tapi prioritaskan dari RTDB
Map<String, dynamic> kustomDefaults = Map.from(parameters["Kustom"]!);
kustomDefaults.addAll(paramsFromRTDB); // Timpa default dengan yang dari RTDB
paramsFromRTDB = kustomDefaults;
// Jika title dari RTDB tidak "Tanaman Lain", pertahankan title dari RTDB
if (paramsFromRTDB['title'] != parameters['Kustom']!['title']) {
// Biarkan title dari RTDB
} else {
paramsFromRTDB['title'] = parameters['Kustom']!['title']; // Pastikan title default jika RTDB juga default
}
}
} else { // Tidak ada title di RTDB
if (determinedPlantType == "Kustom" || (paramsFromRTDB.containsKey('jeda_siram') && paramsFromRTDB.containsKey('waktu_siram'))) {
determinedPlantType = "Kustom";
Map<String, dynamic> kustomDefaults = Map.from(parameters["Kustom"]!);
kustomDefaults.addAll(paramsFromRTDB);
paramsFromRTDB = kustomDefaults;
} else if (parameters.containsKey(determinedPlantType)) { // Misal _activeSelectedPlant dari prefs adalah "Selada Romaine"
Map<String, dynamic> presetDefaults = Map.from(parameters[determinedPlantType]!);
presetDefaults.addAll(paramsFromRTDB);
paramsFromRTDB = presetDefaults;
} else { // Fallback akhir jika determinedPlantType tidak dikenali
determinedPlantType = "Kustom";
paramsFromRTDB = Map.from(parameters["Kustom"]!);
}
}
if (!mapEquals(_activeParameters, paramsFromRTDB) || _activeSelectedPlant != determinedPlantType) {
_activeParameters = paramsFromRTDB;
_activeSelectedPlant = determinedPlantType;
_updateDisplayParametersFromActiveData();
SharedPreferences.getInstance().then((prefs) => prefs.setString('selectedPlant', _activeSelectedPlant));
if (!isInitialLoad) _initializeDraftParametersFromActive(forceOverwrite: false); // Hanya update draft jika bukan load awal & draft tidak diubah user
if (kDebugMode) print("[HandlingProvider] Active params updated from RTDB. Plant: $_activeSelectedPlant, Title: ${_activeParameters['title']}");
}
} else { // Data RTDB kosong atau tidak valid
if (kDebugMode) print("[HandlingProvider] Active parameter data from RTDB is null/empty. Using local for '$_activeSelectedPlant'.");
Map<String,dynamic> fallbackParams = Map.from(parameters[_activeSelectedPlant] ?? parameters["Kustom"]!);
if (!mapEquals(_activeParameters, fallbackParams) || _activeParameters.isEmpty){
_activeParameters = fallbackParams;
_updateDisplayParametersFromActiveData();
if (!isInitialLoad) _initializeDraftParametersFromActive(forceOverwrite: false);
}
}
}
void _updateDisplayParametersFromActiveData() {
if (_activeParameters.isEmpty && _activeSelectedPlant.isNotEmpty && parameters.containsKey(_activeSelectedPlant)) {
_activeParameters = Map.from(parameters[_activeSelectedPlant]!);
} else if (_activeParameters.isEmpty) {
_activeParameters = Map.from(parameters["Kustom"]!);
_activeSelectedPlant = "Kustom";
}
namaTanaman = (_activeParameters['title'] as String?) ?? (_activeSelectedPlant.isNotEmpty ? _activeSelectedPlant : 'Tanaman');
namaLatin = (_activeParameters['latin'] as String?) ?? '...';
gambar = (_activeParameters['thumbnail'] as String?) ?? 'assets/myicon/unknown.png';
idealWaktuSiram = _parseToDouble(_activeParameters['waktu_siram']);
idealJedaSiram = _parseToDouble(_activeParameters['jeda_siram']);
idealSuhu = ((_parseToDouble(_activeParameters['min_suhuair']) + _parseToDouble(_activeParameters['max_suhuair'])) / 2);
idealNutrisi = ((_parseToDouble(_activeParameters['min_tdsair']) + _parseToDouble(_activeParameters['max_tdsair'])) / 2);
double minECVal = (_parseToDouble(_activeParameters['min_tdsair']) / 700.0);
double maxECVal = (_parseToDouble(_activeParameters['max_tdsair']) / 700.0);
idealEC = (minECVal + maxECVal) / 2;
if (hasListeners) notifyListeners();
}
void _initializeDraftParametersFromActive({bool forceOverwrite = false}) {
// forceOverwrite = true akan menimpa draft.
// forceOverwrite = false hanya akan menginisialisasi jika draft kosong atau sama dengan active.
bool shouldUpdateDraft = forceOverwrite ||
_draftParametersForEditing.isEmpty ||
mapEquals(_draftParametersForEditing, _activeParameters);
if (shouldUpdateDraft) {
if (_activeSelectedPlant.isNotEmpty) {
_draftSelectedPlantForEditing = _activeSelectedPlant;
_draftParametersForEditing = Map.from(_activeParameters.isNotEmpty ? _activeParameters : parameters[_activeSelectedPlant] ?? parameters["Kustom"]!);
// Jika yang aktif adalah Kustom, pastikan title, latin, thumb di draft sesuai standar Kustom untuk form
if (_draftSelectedPlantForEditing == "Kustom") {
_draftParametersForEditing['title'] = parameters['Kustom']!['title'];
_draftParametersForEditing['latin'] = parameters['Kustom']!['latin'];
_draftParametersForEditing['thumbnail'] = parameters['Kustom']!['thumbnail'];
}
} else if (parameters.isNotEmpty) {
_draftSelectedPlantForEditing = parameters.keys.firstWhere((k) => k != "Kustom", orElse: () => "Kustom");
_draftParametersForEditing = Map.from(parameters[_draftSelectedPlantForEditing]!);
} else {
_draftSelectedPlantForEditing = "Kustom";
_draftParametersForEditing = Map.from(parameters["Kustom"]!);
}
if (kDebugMode) print("[HandlingProvider] Draft parameters ${_draftParametersForEditing.isEmpty ? 'emptied' : 'initialized/updated'} from active: $_draftSelectedPlantForEditing. Force Overwrite: $forceOverwrite");
if (hasListeners) notifyListeners();
} else {
if (kDebugMode) print("[HandlingProvider] Draft parameters NOT updated from active because draft is dirty or already different.");
}
}
void selectPlantForEditingPage(String plantNameFromDropdown) {
if (kDebugMode) print("[HandlingProvider] User selected '$plantNameFromDropdown' from dropdown for editing.");
_draftSelectedPlantForEditing = plantNameFromDropdown;
if (plantNameFromDropdown == "Kustom") {
// Jika memilih Kustom dari dropdown:
// Ambil basis parameter dari _activeParameters JIKA _activeSelectedPlant saat ini juga Kustom.
// Jika tidak (misal _activeSelectedPlant "Selada", lalu pilih "Kustom" di dropdown),
// maka basisnya adalah default Kustom.
Map<String, dynamic> baseParamsForCustomDraft;
if (_activeSelectedPlant == "Kustom" && _activeParameters.isNotEmpty) {
baseParamsForCustomDraft = Map.from(_activeParameters);
if (kDebugMode) print(" -> Kustom selected. Base from ACTIVE Kustom params.");
} else {
baseParamsForCustomDraft = Map.from(parameters["Kustom"]!);
if (kDebugMode) print(" -> Kustom selected. Base from DEFAULT Kustom params.");
}
_draftParametersForEditing = baseParamsForCustomDraft;
// Selalu set title, latin, thumbnail ke default Kustom untuk form
_draftParametersForEditing['title'] = parameters['Kustom']!['title'];
_draftParametersForEditing['latin'] = parameters['Kustom']!['latin'];
_draftParametersForEditing['thumbnail'] = parameters['Kustom']!['thumbnail'];
} else if (parameters.containsKey(plantNameFromDropdown)) {
_draftParametersForEditing = Map.from(parameters[plantNameFromDropdown]!);
if (kDebugMode) print(" -> Preset '$plantNameFromDropdown' selected. Draft loaded from local preset map.");
} else {
// Fallback, seharusnya tidak terjadi jika dropdown diisi dengan benar
_draftSelectedPlantForEditing = parameters.keys.firstWhere((k) => k != "Kustom", orElse: () => "Kustom");
_draftParametersForEditing = Map.from(parameters[_draftSelectedPlantForEditing]!);
if (kDebugMode) print(" -> Fallback. Draft set to '$_draftSelectedPlantForEditing'.");
}
if (kDebugMode) print(" -> Final draft title for form: ${_draftParametersForEditing['title']}");
notifyListeners();
}
Future<bool> applyDraftPresetToActive(BuildContext context) async {
if (_draftSelectedPlantForEditing == "Kustom" || !parameters.containsKey(_draftSelectedPlantForEditing)) {
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Pilih preset yang valid untuk diterapkan.")));
return false;
}
_activeParameters = Map.from(_draftParametersForEditing);
_activeSelectedPlant = _draftSelectedPlantForEditing;
bool firestoreSuccess = await _saveActiveSelectedPlantToFirestore(context);
bool rtdbSuccess = false;
if (firestoreSuccess) {
rtdbSuccess = await _saveActiveParametersToRTDB(context: context, paramsToSave: _activeParameters);
} else {
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Gagal menyimpan pilihan tanaman ke server.")));
}
if (rtdbSuccess) {
_updateDisplayParametersFromActiveData();
_initializeDraftParametersFromActive(forceOverwrite: true); // Sinkronkan draft setelah berhasil
if (kDebugMode) print("[HandlingProvider] Applied preset '$_activeSelectedPlant' to active and RTDB.");
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text("Parameter untuk '$_activeSelectedPlant' berhasil diupdate ke sistem."),
backgroundColor: Colors.green,
));
}
}
return rtdbSuccess && firestoreSuccess;
}
Future<bool> applyCustomDraftToActive(BuildContext context, Map<String, dynamic> customParamsFromForm) async {
// Update _draftParametersForEditing dengan data dari form, pastikan field meta Kustom ada
Map<String, dynamic> newCustomParams = Map.from(parameters['Kustom']!); // Mulai dengan template Kustom (untuk thumbnail dll)
newCustomParams.addAll(customParamsFromForm); // Timpa dengan nilai dari form
_draftParametersForEditing = newCustomParams;
_draftSelectedPlantForEditing = "Kustom"; // Ini sudah pasti Kustom
_activeParameters = Map.from(_draftParametersForEditing);
_activeSelectedPlant = "Kustom";
bool firestoreSuccess = await _saveActiveSelectedPlantToFirestore(context);
bool rtdbSuccess = false;
if (firestoreSuccess) {
rtdbSuccess = await _saveActiveParametersToRTDB(context: context, paramsToSave: _activeParameters);
} else {
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Gagal menyimpan pilihan 'Kustom' ke server.")));
}
if (rtdbSuccess) {
_updateDisplayParametersFromActiveData();
_initializeDraftParametersFromActive(forceOverwrite: true); // Sinkronkan draft setelah berhasil
if (kDebugMode) print("[HandlingProvider] Applied custom parameters (title: '${_activeParameters['title']}') to active and RTDB.");
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text("Parameter kustom '${_activeParameters['title']}' berhasil diupdate ke sistem."),
backgroundColor: Colors.green,
));
}
}
return rtdbSuccess && firestoreSuccess;
}
Future<bool> _saveActiveSelectedPlantToFirestore(BuildContext context) async {
final uid = AuthService.currentUser?.uid; //
if (uid == null) return false;
if (_activeSelectedPlant.isEmpty) return false;
bool success = await AuthService.updateUserDocument(uid, {'selectedPlant': _activeSelectedPlant}, context); //
if (success) {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('selectedPlant', _activeSelectedPlant);
if (kDebugMode) print("[HandlingProvider] Saved _activeSelectedPlant ('$_activeSelectedPlant') to Firestore & SharedPreferences.");
}
return success;
}
Future<bool> _saveActiveParametersToRTDB({BuildContext? context, required Map<String, dynamic> paramsToSave}) async {
if (!_isSeedKeyReady || _userSeedkey == null || _userSeedkey!.isEmpty || _activeParameterDataRef == null) {
if (context?.mounted ?? false) ScaffoldMessenger.of(context!).showSnackBar(const SnackBar(content: Text("SeedKey/Referensi DB belum siap.")));
return false;
}
if (paramsToSave.isEmpty) {
if (context?.mounted ?? false) ScaffoldMessenger.of(context!).showSnackBar(const SnackBar(content: Text("Tidak ada parameter untuk disimpan.")));
return false;
}
// Pastikan field meta (title, latin, thumbnail) ADA dan BENAR sebelum menyimpan ke RTDB
// Untuk Kustom, title bisa diedit pengguna, jadi ambil dari paramsToSave['title']
// Untuk Preset, title diambil dari nama preset itu sendiri.
Map<String, dynamic> finalParamsToSave = Map.from(paramsToSave);
if (_activeSelectedPlant == "Kustom") {
finalParamsToSave['title'] = paramsToSave['title'] ?? parameters['Kustom']!['title'];
finalParamsToSave['latin'] = paramsToSave['latin'] ?? parameters['Kustom']!['latin'];
finalParamsToSave['thumbnail'] = parameters['Kustom']!['thumbnail']; // Kustom selalu thumbnail default
} else if (parameters.containsKey(_activeSelectedPlant)) {
// Untuk preset, pastikan title, latin, thumbnail konsisten dengan definisi preset
finalParamsToSave['title'] = parameters[_activeSelectedPlant]!['title'];
finalParamsToSave['latin'] = parameters[_activeSelectedPlant]!['latin'];
finalParamsToSave['thumbnail'] = parameters[_activeSelectedPlant]!['thumbnail'];
} else {
// Fallback jika _activeSelectedPlant tidak dikenali (seharusnya tidak terjadi)
finalParamsToSave.putIfAbsent('title', () => parameters['Kustom']!['title']);
finalParamsToSave.putIfAbsent('latin', () => parameters['Kustom']!['latin']);
finalParamsToSave.putIfAbsent('thumbnail', () => parameters['Kustom']!['thumbnail']);
}
try {
await _activeParameterDataRef!.set(finalParamsToSave);
if (kDebugMode) print("[HandlingProvider] Parameters for '${finalParamsToSave['title']}' saved to RTDB.");
return true;
} catch (e) {
if (kDebugMode) print("[HandlingProvider] Failed to save parameters to RTDB: $e");
if (context?.mounted ?? false) {
ScaffoldMessenger.of(context!).showSnackBar(SnackBar(content: Text("Gagal update parameter ke RTDB: ${e.toString()}")));
}
return false;
}
}
void _handleListenerError(String listenerName, Object error) {
if (kDebugMode) print("[$runtimeType] Firebase Listener Error ($listenerName): $error");
if (listenerName == "OnDemandPHResult") {
phMeasurementError = "Gagal membaca hasil pH. Coba lagi.";
isPhMeasuring = false;
}
if (listenerName == "ActiveParameter" && (_activeParameters.isEmpty || _activeSelectedPlant.isEmpty)) {
String plantToUse = _activeSelectedPlant.isNotEmpty && parameters.containsKey(_activeSelectedPlant)
? _activeSelectedPlant
: parameters.keys.firstWhere((k) => k != "Kustom", orElse: () => "Kustom");
_activeSelectedPlant = plantToUse;
_activeParameters = Map.from(parameters[plantToUse]!);
_updateDisplayParametersFromActiveData();
_initializeDraftParametersFromActive(forceOverwrite: true); // Re-init draft jika active gagal load
if (kDebugMode) print("[HandlingProvider] ActiveParameter listener error, fallback to local: $plantToUse");
}
if(hasListeners) notifyListeners();
}
double _parseToDouble(dynamic value) {
if (value is int) return value.toDouble();
if (value is double) return value;
if (value is String) return double.tryParse(value) ?? 0.0;
return 0.0;
}
int _parseToInt(dynamic value) {
if (value is int) return value;
if (value is double) return value.round();
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}
void _handleMonitoringData(DatabaseEvent event) {
final dynamic data = event.snapshot.value;
if (data == null || data is! Map) {
_resetSensorValues();
} else {
ecAir = _parseToDouble(data['ec_air']);
tdsAir = _parseToInt(data['tds_air']);
tinggiAir = _parseToDouble(data['tinggi_air']);
suhuAir = _parseToDouble(data['suhu_air']);
suhuLing = _parseToDouble(data['suhu_ling']);
humiLing = _parseToInt(data['humi_ling']);
}
if (hasListeners) notifyListeners();
}
void _handlePhResult(DatabaseEvent event) {
final dynamic data = event.snapshot.value;
if (data !=null && data is Map) {
final resultData = Map<String, dynamic>.from(data);
onDemandPhValue = _parseToDouble(resultData['ph_val']);
phMeasurementError = null;
} else if (data == null) {
// Keep current onDemandPhValue
} else {
phMeasurementError = "Format hasil pH tidak valid.";
}
isPhMeasuring = false;
if(hasListeners) notifyListeners();
}
Future<void> requestPhMeasurement(BuildContext context) async {
if (!_isSeedKeyReady || _phRequestRef == null) {
if(context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Sistem belum siap untuk pengukuran pH."),
backgroundColor: Colors.orange),
);
}
return;
}
if (isPhMeasuring) {
if(context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Pengukuran pH sedang berlangsung..."), backgroundColor: Colors.blueAccent),
);
}
return;
}
try {
isPhMeasuring = true;
onDemandPhValue = null;
phMeasurementError = null;
if(hasListeners) notifyListeners();
await _phRequestRef!.set(true);
if(context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Permintaan pengukuran pH terkirim. Mohon tunggu..."), backgroundColor: Colors.green),
);
}
} catch (e) {
phMeasurementError = "Gagal mengirim permintaan pH: ${e.toString()}";
isPhMeasuring = false;
if(hasListeners) notifyListeners();
if(context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(phMeasurementError!), backgroundColor: Colors.redAccent),
);
}
}
}
Future<void> updateUserSeedKey(String seedKey) async {
final newSeedKeyValue = seedKey.isNotEmpty ? seedKey : null;
if (_userSeedkey == newSeedKeyValue) return;
_cancelSubscriptions();
_userSeedkey = newSeedKeyValue;
_isSeedKeyReady = newSeedKeyValue != null && newSeedKeyValue.isNotEmpty;
_isLoading = true;
_isInitialized = false;
if(hasListeners) notifyListeners();
// Tidak perlu pass persistedSelectedPlant karena akan diambil dari Firestore/Prefs lagi
await _initializeProviderStates();
}
@override
void dispose() {
_authSubscription?.cancel();
_cancelSubscriptions();
super.dispose();
}
}