722 lines
32 KiB
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();
|
|
}
|
|
} |