diff --git a/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_bloc.dart b/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_bloc.dart index 2b5d172..e1bb8a5 100644 --- a/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_bloc.dart +++ b/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_bloc.dart @@ -20,6 +20,10 @@ class EvaporasiSettingsBloc extends Bloc(_onThresholdTinggiChanged); on(_onRumusChanged); on(_onOffsetChanged); + on(_onPumpStartChanged); + on(_onPumpEndChanged); + on(_onD0Changed); + on(_onDmaxManualChanged); on(_onIntervalRealtimeChanged); on(_onIntervalHistoryChanged); on(_onIntervalBacaChanged); @@ -40,8 +44,10 @@ class EvaporasiSettingsBloc extends Bloc.from(rtSnap.value as Map); + final val = _toInt(rt['dmax_saat_ini'], 0); + final firmware = (rt['firmware_version'] as String?) ?? '--'; + final wifi = rt['wifi_connected'] is bool ? (rt['wifi_connected'] as bool) : (rt['wifi_connected'] == 1); + final firebase = rt['firebase_connected'] is bool ? (rt['firebase_connected'] as bool) : (rt['firebase_connected'] == 1); + final activeD0 = _toInt(rt['d0_active'], state.d0); + final activeDmax = _toInt(rt['dmax_active'], val); + DateTime? lastUpd; + try { + final lu = rt['last_update']; + if (lu != null) { + final ms = (lu is num) ? lu.toInt() : int.tryParse(lu.toString()) ?? 0; + if (ms > 0) lastUpd = DateTime.fromMillisecondsSinceEpoch(ms); + } + } catch (_) {} + + emit(state.copyWith( + dmax: val, + firmwareVersion: firmware, + wifiConnected: wifi ?? false, + firebaseConnected: firebase ?? false, + activeD0: activeD0, + activeDmax: activeDmax, + lastUpdate: lastUpd, + )); + } else { + // fallback: try single path + final snap = await FirebaseDatabase.instance.ref(_rtdbRealtimePath).get(); + final val = snap.exists ? (snap.value as num?)?.toInt() ?? 0 : 0; + emit(state.copyWith(dmax: val)); + } } catch (_) { // ignore: avoid_catching_errors } @@ -101,6 +138,26 @@ class EvaporasiSettingsBloc extends Bloc emit, ) => emit(state.copyWith(intervalBaca_ms: event.value)); + void _onPumpStartChanged( + EvaporasiPumpStartChanged event, + Emitter emit, + ) => emit(state.copyWith(pumpStartTime: event.value)); + + void _onPumpEndChanged( + EvaporasiPumpEndChanged event, + Emitter emit, + ) => emit(state.copyWith(pumpEndTime: event.value)); + + void _onD0Changed( + EvaporasiD0Changed event, + Emitter emit, + ) => emit(state.copyWith(d0: event.value)); + + void _onDmaxManualChanged( + EvaporasiDmaxManualChanged event, + Emitter emit, + ) => emit(state.copyWith(dmaxManual: event.value)); + Future _onDmaxReset( EvaporasiDmaxResetRequested event, Emitter emit, @@ -136,6 +193,10 @@ class EvaporasiSettingsBloc extends Bloc get props => [value]; } +class EvaporasiPumpStartChanged extends EvaporasiSettingsEvent { + final String value; // HH:mm + const EvaporasiPumpStartChanged(this.value); + @override List get props => [value]; +} + +class EvaporasiPumpEndChanged extends EvaporasiSettingsEvent { + final String value; // HH:mm + const EvaporasiPumpEndChanged(this.value); + @override List get props => [value]; +} + +class EvaporasiD0Changed extends EvaporasiSettingsEvent { + final int value; + const EvaporasiD0Changed(this.value); + @override List get props => [value]; +} + +class EvaporasiDmaxManualChanged extends EvaporasiSettingsEvent { + final int value; + const EvaporasiDmaxManualChanged(this.value); + @override List get props => [value]; +} + class EvaporasiSettingsSaved extends EvaporasiSettingsEvent {} class EvaporasiDmaxResetRequested extends EvaporasiSettingsEvent {} \ No newline at end of file diff --git a/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_screen.dart b/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_screen.dart index cab370e..cd48b1e 100644 --- a/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_screen.dart +++ b/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_screen.dart @@ -15,11 +15,20 @@ class _EvaporasiSettingsScreenState extends State { final _rendahController = TextEditingController(); final _tinggiController = TextEditingController(); final _offsetController = TextEditingController(); + final _pumpStartController = TextEditingController(); + final _pumpEndController = TextEditingController(); + final _d0Controller = TextEditingController(); + final _dmaxManualController = TextEditingController(); void _syncControllers(EvaporasiSettingsState s) { final rendah = s.thresholdRendah.toStringAsFixed(1); final tinggi = s.thresholdTinggi.toStringAsFixed(1); final offset = s.koreksiOffset.toStringAsFixed(2); + final pumpStart = s.pumpStartTime; + final pumpEnd = s.pumpEndTime; + final d0 = s.d0 == 0 ? '' : s.d0.toString(); + final dmaxManual = s.dmaxManual == 0 ? '' : s.dmaxManual.toString(); + if (_rendahController.text != rendah) { _rendahController.text = rendah; } @@ -29,46 +38,36 @@ class _EvaporasiSettingsScreenState extends State { if (_offsetController.text != offset) { _offsetController.text = offset; } + if (_pumpStartController.text != pumpStart) { + _pumpStartController.text = pumpStart; + } + if (_pumpEndController.text != pumpEnd) { + _pumpEndController.text = pumpEnd; + } + if (_d0Controller.text != d0) { + _d0Controller.text = d0; + } + if (_dmaxManualController.text != dmaxManual) { + _dmaxManualController.text = dmaxManual; + } } - void _showResetDmaxDialog(BuildContext context) { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - title: const Row(children: [ - Icon(Icons.restart_alt_rounded, color: Colors.orange), - SizedBox(width: 8), - Text('Reset DMAX'), - ]), - content: const Text( - 'DMAX akan direset ke nilai default.\n\n' - 'ESP32 akan mencari nilai DMAX baru secara otomatis ' - 'pada pembacaan berikutnya.\n\n' - 'Lakukan ini hanya jika sensor diganti atau ' - 'kalibrasi ulang diperlukan.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: Text('Batal', style: TextStyle(color: Colors.grey.shade600)), - ), - ElevatedButton.icon( - onPressed: () { - Navigator.pop(ctx); - context.read().add(EvaporasiDmaxResetRequested()); - }, - icon: const Icon(Icons.restart_alt_rounded, size: 16), - label: const Text('Reset DMAX'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange.shade700, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - ), - ], - ), + Future _pickPumpTime(BuildContext context, TextEditingController controller, ValueChanged onSelected, String initialValue) async { + final parts = initialValue.split(':'); + final initial = TimeOfDay( + hour: int.tryParse(parts[0]) ?? 6, + minute: int.tryParse(parts[1]) ?? 0, ); + final picked = await showTimePicker( + context: context, + initialTime: initial, + helpText: 'Pilih Jam Pompa', + ); + if (picked != null) { + final value = '${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}'; + controller.text = value; + onSelected(value); + } } @override @@ -76,6 +75,10 @@ class _EvaporasiSettingsScreenState extends State { _rendahController.dispose(); _tinggiController.dispose(); _offsetController.dispose(); + _pumpStartController.dispose(); + _pumpEndController.dispose(); + _d0Controller.dispose(); + _dmaxManualController.dispose(); super.dispose(); } @@ -256,20 +259,71 @@ class _EvaporasiSettingsScreenState extends State { ), ]), const SizedBox(height: 24), - _sectionTitle('Kalibrasi DMAX'), + _sectionTitle('Kontrol Pompa'), const SizedBox(height: 6), Text( - 'DMAX adalah nilai raw sensor saat panci penuh. ' - 'Diperbarui otomatis oleh ESP32. Reset hanya ' - 'jika sensor diganti atau kalibrasi ulang diperlukan.', + 'Atur jam mulai dan selesai pompa secara langsung dari aplikasi.', style: TextStyle(fontSize: 12, color: Colors.grey.shade500), ), const SizedBox(height: 12), _SettingsCard(children: [ - _DmaxPanel( - dmax: state.dmax, - isResetting: state.isResettingDmax, - onReset: () => _showResetDmaxDialog(context), + _TimePickerField( + controller: _pumpStartController, + label: 'Jam Mulai Pompa', + helper: 'Pilih jam mulai pompa bekerja.', + onTap: () => _pickPumpTime( + context, + _pumpStartController, + (value) => bloc.add(EvaporasiPumpStartChanged(value)), + state.pumpStartTime, + ), + ), + const SizedBox(height: 16), + _TimePickerField( + controller: _pumpEndController, + label: 'Jam Selesai Pompa', + helper: 'Pilih jam berhenti pompa.', + onTap: () => _pickPumpTime( + context, + _pumpEndController, + (value) => bloc.add(EvaporasiPumpEndChanged(value)), + state.pumpEndTime, + ), + ), + ]), + const SizedBox(height: 24), + _sectionTitle('Kalibrasi Raw Sensor'), + const SizedBox(height: 6), + Text( + 'Masukkan nilai D0 dan DMAX secara manual untuk kalibrasi sensor tanpa upload firmware.', + style: TextStyle(fontSize: 12, color: Colors.grey.shade500), + ), + const SizedBox(height: 12), + _SettingsCard(children: [ + _NumericField( + controller: _d0Controller, + label: 'Nilai D0', + hint: 'Contoh: 120', + helper: 'Nilai sensor saat kondisi paling kering.', + onChanged: (v) { + final d = int.tryParse(v); + if (d != null && d >= 0) { + bloc.add(EvaporasiD0Changed(d)); + } + }, + ), + const SizedBox(height: 16), + _NumericField( + controller: _dmaxManualController, + label: 'Nilai DMAX Manual', + hint: 'Contoh: 1023', + helper: 'Kalibrasi nilai maksimum sensor secara manual.', + onChanged: (v) { + final d = int.tryParse(v); + if (d != null && d >= 0) { + bloc.add(EvaporasiDmaxManualChanged(d)); + } + }, ), ]), const SizedBox(height: 24), @@ -588,6 +642,39 @@ class _NumericField extends StatelessWidget { } } +class _TimePickerField extends StatelessWidget { + final TextEditingController controller; + final String label; + final String helper; + final VoidCallback onTap; + + const _TimePickerField({ + required this.controller, + required this.label, + required this.helper, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + readOnly: true, + decoration: InputDecoration( + labelText: label, + hintText: 'HH:mm', + helperText: helper, + helperMaxLines: 2, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), + suffixIcon: const Icon(Icons.schedule_rounded), + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + isDense: true, + ), + onTap: onTap, + ); + } +} + class _IntervalSelector extends StatelessWidget { final String label; final String helper; @@ -647,106 +734,65 @@ class _IntervalSelector extends StatelessWidget { } } -class _DmaxPanel extends StatelessWidget { - final int dmax; - final bool isResetting; - final VoidCallback onReset; +class _RealtimeInfo extends StatelessWidget { + final EvaporasiSettingsState state; + const _RealtimeInfo({required this.state}); - const _DmaxPanel({ - required this.dmax, - required this.isResetting, - required this.onReset, - }); + String _fmtDate(DateTime? d) { + if (d == null) return '--'; + final l = d.toLocal(); + return '${l.day.toString().padLeft(2, '0')}/${l.month.toString().padLeft(2, '0')}/${l.year} ${l.hour.toString().padLeft(2, '0')}:${l.minute.toString().padLeft(2, '0')}:${l.second.toString().padLeft(2, '0')}'; + } @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Nilai DMAX Saat Ini', - style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.black54), - ), - const SizedBox(height: 12), - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [ - Icon(Icons.memory_rounded, size: 15, color: Colors.amber.shade300), - const SizedBox(width: 8), - Text('Raw Sensor Value', - style: TextStyle(fontSize: 12, color: Colors.amber.shade300, fontWeight: FontWeight.bold)), - ]), - const SizedBox(height: 10), - Text( - dmax == 0 ? '-- (belum terbaca)' : dmax.toString(), - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: dmax == 0 ? Colors.grey.shade500 : Colors.green.shade300, - fontFamily: 'monospace', - ), - ), - const SizedBox(height: 4), - Text( - 'Diperbarui otomatis oleh ESP32', - style: TextStyle(fontSize: 11, color: Colors.grey.shade500), - ), - ], - ), - ), - const SizedBox(height: 14), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.orange.shade50, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.orange.shade100), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.info_outline_rounded, size: 16, color: Colors.orange.shade700), - const SizedBox(width: 8), - Expanded( - child: Text( - 'DMAX = nilai ADC saat panci penuh (tinggi air maksimum). ' - 'Semakin besar DMAX, semakin akurat pembacaan tinggi air.', - style: TextStyle(fontSize: 11, color: Colors.orange.shade800), - ), - ), - ], - ), - ), - const SizedBox(height: 14), - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: isResetting ? null : onReset, - icon: isResetting - ? SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.orange.shade700), - ) - : const Icon(Icons.restart_alt_rounded), - label: Text(isResetting ? 'Mereset...' : 'Reset DMAX ke Default'), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.orange.shade700, - side: BorderSide(color: Colors.orange.shade300), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - ), - ), - ], + return Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Icon(Icons.devices_rounded, size: 15, color: Colors.blue.shade700), + const SizedBox(width: 8), + Text('Informasi Perangkat', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: Colors.blue.shade700)), + ]), + const SizedBox(height: 10), + Wrap(spacing: 12, runSpacing: 8, children: [ + _InfoTile(label: 'Firmware', value: state.firmwareVersion), + _InfoTile(label: 'WiFi', value: state.wifiConnected ? 'Terhubung' : 'Tidak'), + _InfoTile(label: 'Firebase', value: state.firebaseConnected ? 'Terhubung' : 'Tidak'), + _InfoTile(label: 'D0 aktif', value: state.activeD0 == 0 ? '--' : state.activeD0.toString()), + _InfoTile(label: 'DMAX aktif', value: state.activeDmax == 0 ? '--' : state.activeDmax.toString()), + _InfoTile(label: 'Terakhir', value: _fmtDate(state.lastUpdate)), + ]) + ]), + ); + } +} + +class _InfoTile extends StatelessWidget { + final String label; + final String value; + const _InfoTile({required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade100), + ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(label, style: TextStyle(fontSize: 11, color: Colors.grey.shade600)), + const SizedBox(height: 6), + Text(value, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold)), + ]), ); } } diff --git a/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_state.dart b/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_state.dart index 0db41bb..c8d735b 100644 --- a/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_state.dart +++ b/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_state.dart @@ -7,8 +7,18 @@ class EvaporasiSettingsState extends Equatable { final double thresholdTinggi; final String rumusKalibrasi; final double koreksiOffset; + final String pumpStartTime; + final String pumpEndTime; + final int d0; + final int dmaxManual; final int dmax; final bool isResettingDmax; + final String firmwareVersion; + final bool wifiConnected; + final bool firebaseConnected; + final int activeD0; + final int activeDmax; + final DateTime? lastUpdate; // Interval (milidetik) — dikirim ke ESP32 via RTDB final int intervalRealtime_ms; // default 300000 = 5 menit @@ -23,13 +33,23 @@ class EvaporasiSettingsState extends Equatable { this.thresholdTinggi = 10.0, this.rumusKalibrasi = 'selisih_max', this.koreksiOffset = 0.0, + this.pumpStartTime = '06:00', + this.pumpEndTime = '18:00', + this.d0 = 0, + this.dmaxManual = 0, this.intervalRealtime_ms = 300000, this.intervalHistory_ms = 600000, this.intervalBaca_ms = 10000, this.status = EvaporasiSettingsStatus.loading, this.errorMessage, - this.dmax = 0, + this.dmax = 0, this.isResettingDmax = false, + this.firmwareVersion = '--', + this.wifiConnected = false, + this.firebaseConnected = false, + this.activeD0 = 0, + this.activeDmax = 0, + this.lastUpdate, }); EvaporasiSettingsState copyWith({ @@ -37,6 +57,10 @@ class EvaporasiSettingsState extends Equatable { double? thresholdTinggi, String? rumusKalibrasi, double? koreksiOffset, + String? pumpStartTime, + String? pumpEndTime, + int? d0, + int? dmaxManual, int? intervalRealtime_ms, int? intervalHistory_ms, int? intervalBaca_ms, @@ -44,12 +68,28 @@ class EvaporasiSettingsState extends Equatable { String? errorMessage, int? dmax, bool? isResettingDmax, + String? firmwareVersion, + bool? wifiConnected, + bool? firebaseConnected, + int? activeD0, + int? activeDmax, + DateTime? lastUpdate, }) { return EvaporasiSettingsState( thresholdRendah: thresholdRendah ?? this.thresholdRendah, thresholdTinggi: thresholdTinggi ?? this.thresholdTinggi, rumusKalibrasi: rumusKalibrasi ?? this.rumusKalibrasi, koreksiOffset: koreksiOffset ?? this.koreksiOffset, + pumpStartTime: pumpStartTime ?? this.pumpStartTime, + pumpEndTime: pumpEndTime ?? this.pumpEndTime, + d0: d0 ?? this.d0, + dmaxManual: dmaxManual ?? this.dmaxManual, + firmwareVersion: firmwareVersion ?? this.firmwareVersion, + wifiConnected: wifiConnected ?? this.wifiConnected, + firebaseConnected: firebaseConnected ?? this.firebaseConnected, + activeD0: activeD0 ?? this.activeD0, + activeDmax: activeDmax ?? this.activeDmax, + lastUpdate: lastUpdate ?? this.lastUpdate, intervalRealtime_ms: intervalRealtime_ms ?? this.intervalRealtime_ms, intervalHistory_ms: intervalHistory_ms ?? this.intervalHistory_ms, intervalBaca_ms: intervalBaca_ms ?? this.intervalBaca_ms, @@ -63,6 +103,8 @@ class EvaporasiSettingsState extends Equatable { @override List get props => [ thresholdRendah, thresholdTinggi, rumusKalibrasi, koreksiOffset, + pumpStartTime, pumpEndTime, d0, dmaxManual, + firmwareVersion, wifiConnected, firebaseConnected, activeD0, activeDmax, lastUpdate, intervalRealtime_ms, intervalHistory_ms, intervalBaca_ms, status, errorMessage, dmax, isResettingDmax, diff --git a/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart b/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart index 814577a..c405d5d 100644 --- a/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart +++ b/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart @@ -9,9 +9,9 @@ import '../blocs/evaporasi_bloc.dart'; import '../../shared/utils/pdf/pdf_export_service.dart'; import '../../shared/widgets/export_pdf_button.dart'; import 'widgets/evaporasi_chart_widget.dart'; +import 'widgets/evaporasi_control_panel.dart'; import 'widgets/evaporasi_range_selector.dart'; import 'widgets/evaporasi_history_list.dart'; -// TAMBAH baris ini setelah import yang sudah ada: import '../../shared/utils/excel/evaporasi_excel_service.dart'; class EvaporasiScreen extends StatefulWidget { @@ -319,19 +319,18 @@ class _EvaporasiScreenState extends State { : () => _showExportDialog(context, state), ), ), - IconButton( - tooltip: 'Pengaturan', - icon: const Icon(Icons.settings_outlined), - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const EvaporasiSettingsScreen(), + IconButton( + tooltip: 'Pengaturan', + icon: const Icon(Icons.settings_outlined), + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const EvaporasiSettingsScreen(), + ), + ), + ), + ], ), - ), - ), -], - ), - body: BlocBuilder( builder: (context, state) { if (state.isLoading) { @@ -364,6 +363,10 @@ class _EvaporasiScreenState extends State { _statusCard(state), const SizedBox(height: 20), + // ── Kontrol Database Evaporasi ─────────────── + const EvaporasiControlPanel(), + const SizedBox(height: 20), + // ── Range Selector ────────────────────────── const Text( 'Tren Evaporasi & Suhu', @@ -449,7 +452,6 @@ class _EvaporasiScreenState extends State { ), const SizedBox(height: 20), - // ── Riwayat Data ──────────────────────────── BlocBuilder( builder: (context, s) => EvaporasiHistoryList( @@ -597,7 +599,6 @@ class _EvaporasiScreenState extends State { ); } - Widget _statusCard(EvaporasiState state) { Color statusColor; IconData statusIcon; diff --git a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_control_panel.dart b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_control_panel.dart new file mode 100644 index 0000000..87e3c82 --- /dev/null +++ b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_control_panel.dart @@ -0,0 +1,227 @@ +import 'dart:async'; + +import 'package:firebase_database/firebase_database.dart'; +import 'package:flutter/material.dart'; + +class EvaporasiControlPanel extends StatefulWidget { + const EvaporasiControlPanel({super.key}); + + @override + State createState() => _EvaporasiControlPanelState(); +} + +class _EvaporasiControlPanelState extends State { + bool _selenoid = false; + bool _isTogglingSelenoid = false; + bool _isResettingEvaporasi = false; + StreamSubscription? _subscription; + + @override + void initState() { + super.initState(); + _subscription = FirebaseDatabase.instance + .ref('Monitoring/realtime') + .onValue + .listen(_onRealtimeUpdated); + } + + void _onRealtimeUpdated(DatabaseEvent event) { + final data = event.snapshot.value; + if (data is Map) { + final rawSelenoid = data['selenoid']; + final selenoid = _toBool(rawSelenoid, _selenoid); + if (mounted) { + setState(() { + _selenoid = selenoid; + }); + } + } + } + + bool _toBool(dynamic raw, bool fallback) { + if (raw is bool) return raw; + if (raw is num) return raw != 0; + if (raw is String) { + final lower = raw.toLowerCase(); + return lower == 'true' || lower == '1'; + } + return fallback; + } + + Future _toggleSelenoid() async { + if (_isTogglingSelenoid) return; + final nextValue = !_selenoid; + setState(() { + _isTogglingSelenoid = true; + }); + + try { + await Future.wait([ + FirebaseDatabase.instance.ref('Monitoring/selenoid').set(nextValue), + FirebaseDatabase.instance + .ref('Monitoring/realtime/selenoid') + .set(nextValue), + ]); + if (mounted) { + setState(() { + _selenoid = nextValue; + }); + } + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: + Text('Perintah selenoid dikirim: ${nextValue ? 'ON' : 'OFF'}'), + behavior: SnackBarBehavior.floating, + )); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Gagal mengirim perintah selenoid: $e'), + backgroundColor: Colors.red.shade600, + behavior: SnackBarBehavior.floating, + )); + } + } finally { + if (mounted) { + setState(() { + _isTogglingSelenoid = false; + }); + } + } + } + + Future _resetEvaporasi() async { + if (_isResettingEvaporasi) return; + setState(() { + _isResettingEvaporasi = true; + }); + + try { + await Future.wait([ + FirebaseDatabase.instance.ref('Monitoring/reset_evaporasi').set(true), + FirebaseDatabase.instance + .ref('Monitoring/realtime/reset_evaporasi') + .set(true), + ]); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Perintah reset evaporasi berhasil dikirim.'), + behavior: SnackBarBehavior.floating, + )); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Gagal mengirim perintah reset evaporasi: $e'), + backgroundColor: Colors.red.shade600, + behavior: SnackBarBehavior.floating, + )); + } + } finally { + if (mounted) { + setState(() { + _isResettingEvaporasi = false; + }); + } + } + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.api_rounded, color: Colors.blue.shade700), + const SizedBox(width: 10), + const Expanded( + child: Text( + 'Kontrol Database Evaporasi', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + 'Tombol ini terhubung ke path Firebase yang belum ada di UI utama: reset evaporasi dan kontrol selenoid.', + style: TextStyle(fontSize: 12, color: Colors.grey.shade600), + ), + const SizedBox(height: 16), + Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ElevatedButton.icon( + onPressed: _isResettingEvaporasi ? null : _resetEvaporasi, + icon: _isResettingEvaporasi + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, color: Colors.white), + ) + : const Icon(Icons.refresh_rounded), + label: Text( + _isResettingEvaporasi ? 'Mengirim...' : 'Reset Evaporasi'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade600, + foregroundColor: Colors.white, + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + ), + OutlinedButton.icon( + onPressed: _isTogglingSelenoid ? null : _toggleSelenoid, + icon: _isTogglingSelenoid + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Icon( + _selenoid + ? Icons.toggle_on_rounded + : Icons.toggle_off_rounded, + color: _selenoid ? Colors.green : Colors.grey), + label: + Text(_selenoid ? 'Matikan Selenoid' : 'Nyalakan Selenoid'), + style: OutlinedButton.styleFrom( + foregroundColor: + _selenoid ? Colors.green.shade700 : Colors.grey.shade800, + side: BorderSide( + color: _selenoid + ? Colors.green.shade300 + : Colors.grey.shade300), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + ), + ], + ), + const SizedBox(height: 16), + Text( + 'Status selenoid saat ini: ${_selenoid ? 'AKTIF' : 'MATI'}', + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), + ), + ], + ), + ); + } +}