Merge branch 'branch' of https://github.com/kleponijo/klimatologi into branch_percobaan
This commit is contained in:
commit
221f34a236
|
|
@ -20,6 +20,10 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
||||||
on<EvaporasiThresholdTinggiChanged>(_onThresholdTinggiChanged);
|
on<EvaporasiThresholdTinggiChanged>(_onThresholdTinggiChanged);
|
||||||
on<EvaporasiRumusKalibrasiChanged>(_onRumusChanged);
|
on<EvaporasiRumusKalibrasiChanged>(_onRumusChanged);
|
||||||
on<EvaporasiKoreksiOffsetChanged>(_onOffsetChanged);
|
on<EvaporasiKoreksiOffsetChanged>(_onOffsetChanged);
|
||||||
|
on<EvaporasiPumpStartChanged>(_onPumpStartChanged);
|
||||||
|
on<EvaporasiPumpEndChanged>(_onPumpEndChanged);
|
||||||
|
on<EvaporasiD0Changed>(_onD0Changed);
|
||||||
|
on<EvaporasiDmaxManualChanged>(_onDmaxManualChanged);
|
||||||
on<EvaporasiIntervalRealtimeChanged>(_onIntervalRealtimeChanged);
|
on<EvaporasiIntervalRealtimeChanged>(_onIntervalRealtimeChanged);
|
||||||
on<EvaporasiIntervalHistoryChanged>(_onIntervalHistoryChanged);
|
on<EvaporasiIntervalHistoryChanged>(_onIntervalHistoryChanged);
|
||||||
on<EvaporasiIntervalBacaChanged>(_onIntervalBacaChanged);
|
on<EvaporasiIntervalBacaChanged>(_onIntervalBacaChanged);
|
||||||
|
|
@ -40,8 +44,10 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
||||||
thresholdRendah: _toDouble(data['threshold_rendah'], 2.0),
|
thresholdRendah: _toDouble(data['threshold_rendah'], 2.0),
|
||||||
thresholdTinggi: _toDouble(data['threshold_tinggi'], 10.0),
|
thresholdTinggi: _toDouble(data['threshold_tinggi'], 10.0),
|
||||||
rumusKalibrasi: (data['rumus_kalibrasi'] as String?) ?? 'selisih_max',
|
rumusKalibrasi: (data['rumus_kalibrasi'] as String?) ?? 'selisih_max',
|
||||||
koreksiOffset: _toDouble(data['koreksi_offset'], 0.0),
|
koreksiOffset: _toDouble(data['koreksi_offset'], 0.0), pumpStartTime: (data['pump_start_time'] as String?) ?? '06:00',
|
||||||
intervalRealtime_ms: _toInt(data['interval_realtime_ms'], 300000),
|
pumpEndTime: (data['pump_end_time'] as String?) ?? '18:00',
|
||||||
|
d0: _toInt(data['d0'], 0),
|
||||||
|
dmaxManual: _toInt(data['dmax_manual'], 0), intervalRealtime_ms: _toInt(data['interval_realtime_ms'], 300000),
|
||||||
intervalHistory_ms: _toInt(data['interval_history_ms'], 600000),
|
intervalHistory_ms: _toInt(data['interval_history_ms'], 600000),
|
||||||
intervalBaca_ms: _toInt(data['interval_baca_ms'], 10000),
|
intervalBaca_ms: _toInt(data['interval_baca_ms'], 10000),
|
||||||
status: EvaporasiSettingsStatus.loaded,
|
status: EvaporasiSettingsStatus.loaded,
|
||||||
|
|
@ -58,9 +64,40 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final rtSnap = await FirebaseDatabase.instance.ref(_rtdbRealtimePath).get();
|
final rtRef = FirebaseDatabase.instance.ref('Monitoring/realtime');
|
||||||
final val = rtSnap.exists ? (rtSnap.value as num?)?.toInt() ?? 0 : 0;
|
final rtSnap = await rtRef.get();
|
||||||
|
if (rtSnap.exists && rtSnap.value != null) {
|
||||||
|
final rt = Map<String, dynamic>.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));
|
emit(state.copyWith(dmax: val));
|
||||||
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// ignore: avoid_catching_errors
|
// ignore: avoid_catching_errors
|
||||||
}
|
}
|
||||||
|
|
@ -101,6 +138,26 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
||||||
Emitter<EvaporasiSettingsState> emit,
|
Emitter<EvaporasiSettingsState> emit,
|
||||||
) => emit(state.copyWith(intervalBaca_ms: event.value));
|
) => emit(state.copyWith(intervalBaca_ms: event.value));
|
||||||
|
|
||||||
|
void _onPumpStartChanged(
|
||||||
|
EvaporasiPumpStartChanged event,
|
||||||
|
Emitter<EvaporasiSettingsState> emit,
|
||||||
|
) => emit(state.copyWith(pumpStartTime: event.value));
|
||||||
|
|
||||||
|
void _onPumpEndChanged(
|
||||||
|
EvaporasiPumpEndChanged event,
|
||||||
|
Emitter<EvaporasiSettingsState> emit,
|
||||||
|
) => emit(state.copyWith(pumpEndTime: event.value));
|
||||||
|
|
||||||
|
void _onD0Changed(
|
||||||
|
EvaporasiD0Changed event,
|
||||||
|
Emitter<EvaporasiSettingsState> emit,
|
||||||
|
) => emit(state.copyWith(d0: event.value));
|
||||||
|
|
||||||
|
void _onDmaxManualChanged(
|
||||||
|
EvaporasiDmaxManualChanged event,
|
||||||
|
Emitter<EvaporasiSettingsState> emit,
|
||||||
|
) => emit(state.copyWith(dmaxManual: event.value));
|
||||||
|
|
||||||
Future<void> _onDmaxReset(
|
Future<void> _onDmaxReset(
|
||||||
EvaporasiDmaxResetRequested event,
|
EvaporasiDmaxResetRequested event,
|
||||||
Emitter<EvaporasiSettingsState> emit,
|
Emitter<EvaporasiSettingsState> emit,
|
||||||
|
|
@ -136,6 +193,10 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
||||||
'threshold_tinggi': state.thresholdTinggi,
|
'threshold_tinggi': state.thresholdTinggi,
|
||||||
'rumus_kalibrasi': state.rumusKalibrasi,
|
'rumus_kalibrasi': state.rumusKalibrasi,
|
||||||
'koreksi_offset': state.koreksiOffset,
|
'koreksi_offset': state.koreksiOffset,
|
||||||
|
'pump_start_time': state.pumpStartTime,
|
||||||
|
'pump_end_time': state.pumpEndTime,
|
||||||
|
'd0': state.d0,
|
||||||
|
'dmax_manual': state.dmaxManual,
|
||||||
'interval_realtime_ms': state.intervalRealtime_ms,
|
'interval_realtime_ms': state.intervalRealtime_ms,
|
||||||
'interval_history_ms': state.intervalHistory_ms,
|
'interval_history_ms': state.intervalHistory_ms,
|
||||||
'interval_baca_ms': state.intervalBaca_ms,
|
'interval_baca_ms': state.intervalBaca_ms,
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,30 @@ class EvaporasiIntervalBacaChanged extends EvaporasiSettingsEvent {
|
||||||
@override List<Object?> get props => [value];
|
@override List<Object?> get props => [value];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EvaporasiPumpStartChanged extends EvaporasiSettingsEvent {
|
||||||
|
final String value; // HH:mm
|
||||||
|
const EvaporasiPumpStartChanged(this.value);
|
||||||
|
@override List<Object?> get props => [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
class EvaporasiPumpEndChanged extends EvaporasiSettingsEvent {
|
||||||
|
final String value; // HH:mm
|
||||||
|
const EvaporasiPumpEndChanged(this.value);
|
||||||
|
@override List<Object?> get props => [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
class EvaporasiD0Changed extends EvaporasiSettingsEvent {
|
||||||
|
final int value;
|
||||||
|
const EvaporasiD0Changed(this.value);
|
||||||
|
@override List<Object?> get props => [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
class EvaporasiDmaxManualChanged extends EvaporasiSettingsEvent {
|
||||||
|
final int value;
|
||||||
|
const EvaporasiDmaxManualChanged(this.value);
|
||||||
|
@override List<Object?> get props => [value];
|
||||||
|
}
|
||||||
|
|
||||||
class EvaporasiSettingsSaved extends EvaporasiSettingsEvent {}
|
class EvaporasiSettingsSaved extends EvaporasiSettingsEvent {}
|
||||||
|
|
||||||
class EvaporasiDmaxResetRequested extends EvaporasiSettingsEvent {}
|
class EvaporasiDmaxResetRequested extends EvaporasiSettingsEvent {}
|
||||||
|
|
@ -15,11 +15,20 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
||||||
final _rendahController = TextEditingController();
|
final _rendahController = TextEditingController();
|
||||||
final _tinggiController = TextEditingController();
|
final _tinggiController = TextEditingController();
|
||||||
final _offsetController = TextEditingController();
|
final _offsetController = TextEditingController();
|
||||||
|
final _pumpStartController = TextEditingController();
|
||||||
|
final _pumpEndController = TextEditingController();
|
||||||
|
final _d0Controller = TextEditingController();
|
||||||
|
final _dmaxManualController = TextEditingController();
|
||||||
|
|
||||||
void _syncControllers(EvaporasiSettingsState s) {
|
void _syncControllers(EvaporasiSettingsState s) {
|
||||||
final rendah = s.thresholdRendah.toStringAsFixed(1);
|
final rendah = s.thresholdRendah.toStringAsFixed(1);
|
||||||
final tinggi = s.thresholdTinggi.toStringAsFixed(1);
|
final tinggi = s.thresholdTinggi.toStringAsFixed(1);
|
||||||
final offset = s.koreksiOffset.toStringAsFixed(2);
|
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) {
|
if (_rendahController.text != rendah) {
|
||||||
_rendahController.text = rendah;
|
_rendahController.text = rendah;
|
||||||
}
|
}
|
||||||
|
|
@ -29,46 +38,36 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
||||||
if (_offsetController.text != offset) {
|
if (_offsetController.text != offset) {
|
||||||
_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) {
|
Future<void> _pickPumpTime(BuildContext context, TextEditingController controller, ValueChanged<String> onSelected, String initialValue) async {
|
||||||
showDialog(
|
final parts = initialValue.split(':');
|
||||||
context: context,
|
final initial = TimeOfDay(
|
||||||
builder: (ctx) => AlertDialog(
|
hour: int.tryParse(parts[0]) ?? 6,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
minute: int.tryParse(parts[1]) ?? 0,
|
||||||
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<EvaporasiSettingsBloc>().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)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
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
|
@override
|
||||||
|
|
@ -76,6 +75,10 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
||||||
_rendahController.dispose();
|
_rendahController.dispose();
|
||||||
_tinggiController.dispose();
|
_tinggiController.dispose();
|
||||||
_offsetController.dispose();
|
_offsetController.dispose();
|
||||||
|
_pumpStartController.dispose();
|
||||||
|
_pumpEndController.dispose();
|
||||||
|
_d0Controller.dispose();
|
||||||
|
_dmaxManualController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,20 +259,71 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_sectionTitle('Kalibrasi DMAX'),
|
_sectionTitle('Kontrol Pompa'),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
'DMAX adalah nilai raw sensor saat panci penuh. '
|
'Atur jam mulai dan selesai pompa secara langsung dari aplikasi.',
|
||||||
'Diperbarui otomatis oleh ESP32. Reset hanya '
|
|
||||||
'jika sensor diganti atau kalibrasi ulang diperlukan.',
|
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_SettingsCard(children: [
|
_SettingsCard(children: [
|
||||||
_DmaxPanel(
|
_TimePickerField(
|
||||||
dmax: state.dmax,
|
controller: _pumpStartController,
|
||||||
isResetting: state.isResettingDmax,
|
label: 'Jam Mulai Pompa',
|
||||||
onReset: () => _showResetDmaxDialog(context),
|
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),
|
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 {
|
class _IntervalSelector extends StatelessWidget {
|
||||||
final String label;
|
final String label;
|
||||||
final String helper;
|
final String helper;
|
||||||
|
|
@ -647,106 +734,65 @@ class _IntervalSelector extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DmaxPanel extends StatelessWidget {
|
class _RealtimeInfo extends StatelessWidget {
|
||||||
final int dmax;
|
final EvaporasiSettingsState state;
|
||||||
final bool isResetting;
|
const _RealtimeInfo({required this.state});
|
||||||
final VoidCallback onReset;
|
|
||||||
|
|
||||||
const _DmaxPanel({
|
String _fmtDate(DateTime? d) {
|
||||||
required this.dmax,
|
if (d == null) return '--';
|
||||||
required this.isResetting,
|
final l = d.toLocal();
|
||||||
required this.onReset,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Container(
|
||||||
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,
|
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),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.orange.shade50,
|
color: Colors.grey.shade50,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: Colors.orange.shade100),
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Row(children: [
|
||||||
children: [
|
Icon(Icons.devices_rounded, size: 15, color: Colors.blue.shade700),
|
||||||
Icon(Icons.info_outline_rounded, size: 16, color: Colors.orange.shade700),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Text('Informasi Perangkat', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: Colors.blue.shade700)),
|
||||||
child: Text(
|
]),
|
||||||
'DMAX = nilai ADC saat panci penuh (tinggi air maksimum). '
|
const SizedBox(height: 10),
|
||||||
'Semakin besar DMAX, semakin akurat pembacaan tinggi air.',
|
Wrap(spacing: 12, runSpacing: 8, children: [
|
||||||
style: TextStyle(fontSize: 11, color: Colors.orange.shade800),
|
_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)),
|
||||||
const SizedBox(height: 14),
|
])
|
||||||
SizedBox(
|
]),
|
||||||
width: double.infinity,
|
);
|
||||||
child: OutlinedButton.icon(
|
}
|
||||||
onPressed: isResetting ? null : onReset,
|
}
|
||||||
icon: isResetting
|
|
||||||
? SizedBox(
|
class _InfoTile extends StatelessWidget {
|
||||||
width: 16,
|
final String label;
|
||||||
height: 16,
|
final String value;
|
||||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.orange.shade700),
|
const _InfoTile({required this.label, required this.value});
|
||||||
)
|
|
||||||
: const Icon(Icons.restart_alt_rounded),
|
@override
|
||||||
label: Text(isResetting ? 'Mereset...' : 'Reset DMAX ke Default'),
|
Widget build(BuildContext context) {
|
||||||
style: OutlinedButton.styleFrom(
|
return Container(
|
||||||
foregroundColor: Colors.orange.shade700,
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
side: BorderSide(color: Colors.orange.shade300),
|
decoration: BoxDecoration(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
color: Colors.white,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
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)),
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,18 @@ class EvaporasiSettingsState extends Equatable {
|
||||||
final double thresholdTinggi;
|
final double thresholdTinggi;
|
||||||
final String rumusKalibrasi;
|
final String rumusKalibrasi;
|
||||||
final double koreksiOffset;
|
final double koreksiOffset;
|
||||||
|
final String pumpStartTime;
|
||||||
|
final String pumpEndTime;
|
||||||
|
final int d0;
|
||||||
|
final int dmaxManual;
|
||||||
final int dmax;
|
final int dmax;
|
||||||
final bool isResettingDmax;
|
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
|
// Interval (milidetik) — dikirim ke ESP32 via RTDB
|
||||||
final int intervalRealtime_ms; // default 300000 = 5 menit
|
final int intervalRealtime_ms; // default 300000 = 5 menit
|
||||||
|
|
@ -23,6 +33,10 @@ class EvaporasiSettingsState extends Equatable {
|
||||||
this.thresholdTinggi = 10.0,
|
this.thresholdTinggi = 10.0,
|
||||||
this.rumusKalibrasi = 'selisih_max',
|
this.rumusKalibrasi = 'selisih_max',
|
||||||
this.koreksiOffset = 0.0,
|
this.koreksiOffset = 0.0,
|
||||||
|
this.pumpStartTime = '06:00',
|
||||||
|
this.pumpEndTime = '18:00',
|
||||||
|
this.d0 = 0,
|
||||||
|
this.dmaxManual = 0,
|
||||||
this.intervalRealtime_ms = 300000,
|
this.intervalRealtime_ms = 300000,
|
||||||
this.intervalHistory_ms = 600000,
|
this.intervalHistory_ms = 600000,
|
||||||
this.intervalBaca_ms = 10000,
|
this.intervalBaca_ms = 10000,
|
||||||
|
|
@ -30,6 +44,12 @@ class EvaporasiSettingsState extends Equatable {
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
this.dmax = 0,
|
this.dmax = 0,
|
||||||
this.isResettingDmax = false,
|
this.isResettingDmax = false,
|
||||||
|
this.firmwareVersion = '--',
|
||||||
|
this.wifiConnected = false,
|
||||||
|
this.firebaseConnected = false,
|
||||||
|
this.activeD0 = 0,
|
||||||
|
this.activeDmax = 0,
|
||||||
|
this.lastUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
EvaporasiSettingsState copyWith({
|
EvaporasiSettingsState copyWith({
|
||||||
|
|
@ -37,6 +57,10 @@ class EvaporasiSettingsState extends Equatable {
|
||||||
double? thresholdTinggi,
|
double? thresholdTinggi,
|
||||||
String? rumusKalibrasi,
|
String? rumusKalibrasi,
|
||||||
double? koreksiOffset,
|
double? koreksiOffset,
|
||||||
|
String? pumpStartTime,
|
||||||
|
String? pumpEndTime,
|
||||||
|
int? d0,
|
||||||
|
int? dmaxManual,
|
||||||
int? intervalRealtime_ms,
|
int? intervalRealtime_ms,
|
||||||
int? intervalHistory_ms,
|
int? intervalHistory_ms,
|
||||||
int? intervalBaca_ms,
|
int? intervalBaca_ms,
|
||||||
|
|
@ -44,12 +68,28 @@ class EvaporasiSettingsState extends Equatable {
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
int? dmax,
|
int? dmax,
|
||||||
bool? isResettingDmax,
|
bool? isResettingDmax,
|
||||||
|
String? firmwareVersion,
|
||||||
|
bool? wifiConnected,
|
||||||
|
bool? firebaseConnected,
|
||||||
|
int? activeD0,
|
||||||
|
int? activeDmax,
|
||||||
|
DateTime? lastUpdate,
|
||||||
}) {
|
}) {
|
||||||
return EvaporasiSettingsState(
|
return EvaporasiSettingsState(
|
||||||
thresholdRendah: thresholdRendah ?? this.thresholdRendah,
|
thresholdRendah: thresholdRendah ?? this.thresholdRendah,
|
||||||
thresholdTinggi: thresholdTinggi ?? this.thresholdTinggi,
|
thresholdTinggi: thresholdTinggi ?? this.thresholdTinggi,
|
||||||
rumusKalibrasi: rumusKalibrasi ?? this.rumusKalibrasi,
|
rumusKalibrasi: rumusKalibrasi ?? this.rumusKalibrasi,
|
||||||
koreksiOffset: koreksiOffset ?? this.koreksiOffset,
|
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,
|
intervalRealtime_ms: intervalRealtime_ms ?? this.intervalRealtime_ms,
|
||||||
intervalHistory_ms: intervalHistory_ms ?? this.intervalHistory_ms,
|
intervalHistory_ms: intervalHistory_ms ?? this.intervalHistory_ms,
|
||||||
intervalBaca_ms: intervalBaca_ms ?? this.intervalBaca_ms,
|
intervalBaca_ms: intervalBaca_ms ?? this.intervalBaca_ms,
|
||||||
|
|
@ -63,6 +103,8 @@ class EvaporasiSettingsState extends Equatable {
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
thresholdRendah, thresholdTinggi, rumusKalibrasi, koreksiOffset,
|
thresholdRendah, thresholdTinggi, rumusKalibrasi, koreksiOffset,
|
||||||
|
pumpStartTime, pumpEndTime, d0, dmaxManual,
|
||||||
|
firmwareVersion, wifiConnected, firebaseConnected, activeD0, activeDmax, lastUpdate,
|
||||||
intervalRealtime_ms, intervalHistory_ms, intervalBaca_ms,
|
intervalRealtime_ms, intervalHistory_ms, intervalBaca_ms,
|
||||||
status, errorMessage,
|
status, errorMessage,
|
||||||
dmax, isResettingDmax,
|
dmax, isResettingDmax,
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ import '../blocs/evaporasi_bloc.dart';
|
||||||
import '../../shared/utils/pdf/pdf_export_service.dart';
|
import '../../shared/utils/pdf/pdf_export_service.dart';
|
||||||
import '../../shared/widgets/export_pdf_button.dart';
|
import '../../shared/widgets/export_pdf_button.dart';
|
||||||
import 'widgets/evaporasi_chart_widget.dart';
|
import 'widgets/evaporasi_chart_widget.dart';
|
||||||
|
import 'widgets/evaporasi_control_panel.dart';
|
||||||
import 'widgets/evaporasi_range_selector.dart';
|
import 'widgets/evaporasi_range_selector.dart';
|
||||||
import 'widgets/evaporasi_history_list.dart';
|
import 'widgets/evaporasi_history_list.dart';
|
||||||
// TAMBAH baris ini setelah import yang sudah ada:
|
|
||||||
import '../../shared/utils/excel/evaporasi_excel_service.dart';
|
import '../../shared/utils/excel/evaporasi_excel_service.dart';
|
||||||
|
|
||||||
class EvaporasiScreen extends StatefulWidget {
|
class EvaporasiScreen extends StatefulWidget {
|
||||||
|
|
@ -331,7 +331,6 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
body: BlocBuilder<EvaporasiBloc, EvaporasiState>(
|
body: BlocBuilder<EvaporasiBloc, EvaporasiState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
|
|
@ -364,6 +363,10 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
|
||||||
_statusCard(state),
|
_statusCard(state),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// ── Kontrol Database Evaporasi ───────────────
|
||||||
|
const EvaporasiControlPanel(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// ── Range Selector ──────────────────────────
|
// ── Range Selector ──────────────────────────
|
||||||
const Text(
|
const Text(
|
||||||
'Tren Evaporasi & Suhu',
|
'Tren Evaporasi & Suhu',
|
||||||
|
|
@ -449,7 +452,6 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
|
||||||
// ── Riwayat Data ────────────────────────────
|
// ── Riwayat Data ────────────────────────────
|
||||||
BlocBuilder<EvaporasiBloc, EvaporasiState>(
|
BlocBuilder<EvaporasiBloc, EvaporasiState>(
|
||||||
builder: (context, s) => EvaporasiHistoryList(
|
builder: (context, s) => EvaporasiHistoryList(
|
||||||
|
|
@ -597,7 +599,6 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Widget _statusCard(EvaporasiState state) {
|
Widget _statusCard(EvaporasiState state) {
|
||||||
Color statusColor;
|
Color statusColor;
|
||||||
IconData statusIcon;
|
IconData statusIcon;
|
||||||
|
|
|
||||||
|
|
@ -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<EvaporasiControlPanel> createState() => _EvaporasiControlPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EvaporasiControlPanelState extends State<EvaporasiControlPanel> {
|
||||||
|
bool _selenoid = false;
|
||||||
|
bool _isTogglingSelenoid = false;
|
||||||
|
bool _isResettingEvaporasi = false;
|
||||||
|
StreamSubscription<DatabaseEvent>? _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<void> _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<void> _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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue