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 _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 _draftParametersForEditing = {}; String _draftSelectedPlantForEditing = ''; // --- State Lainnya --- String? _userSeedkey; bool _isInitialized = false; bool _isLoading = true; bool _isSeedKeyReady = false; double? onDemandPhValue; bool isPhMeasuring = false; String? phMeasurementError; StreamSubscription? _monitoringSubscription; StreamSubscription? _parameterSubscription; StreamSubscription? _onDemandPhSubscription; StreamSubscription? _authSubscription; final Map> 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 get activeParametersReadonly => Map.unmodifiable(_activeParameters); String get draftSelectedPlantForEditing => _draftSelectedPlantForEditing; Map get draftParametersForEditingReadonly => Map.unmodifiable(_draftParametersForEditing); Map 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?; 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 _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 _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 _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 paramsFromRTDB; String determinedPlantType = _activeSelectedPlant; // Mulai dengan apa yang sudah ada (dari Prefs/Firestore) if (data != null && data is Map && data.isNotEmpty) { paramsFromRTDB = Map.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 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 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 kustomDefaults = Map.from(parameters["Kustom"]!); kustomDefaults.addAll(paramsFromRTDB); paramsFromRTDB = kustomDefaults; } else if (parameters.containsKey(determinedPlantType)) { // Misal _activeSelectedPlant dari prefs adalah "Selada Romaine" Map 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 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 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 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 applyCustomDraftToActive(BuildContext context, Map customParamsFromForm) async { // Update _draftParametersForEditing dengan data dari form, pastikan field meta Kustom ada Map 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 _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 _saveActiveParametersToRTDB({BuildContext? context, required Map 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 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.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 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 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(); } }