From 096a0fd9286ef1951708c469ad269fed54faeb43 Mon Sep 17 00:00:00 2001 From: Mochamad ongki ramadani Date: Sun, 21 Jun 2026 12:54:32 +0700 Subject: [PATCH] pepe --- .../blocs/evaporasi_settings_bloc.dart | 23 ++- .../blocs/evaporasi_settings_event.dart | 5 - .../blocs/evaporasi_settings_screen.dart | 143 +-------------- .../blocs/evaporasi_settings_state.dart | 6 +- .../evaporasi/blocs/evaporasi_bloc.dart | 62 ------- .../evaporasi/views/evaporasi_screen.dart | 4 - .../views/widgets/evaporasi_history_list.dart | 167 ++++++++++++------ 7 files changed, 126 insertions(+), 284 deletions(-) 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 85e1d11..1d7855a 100644 --- a/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_bloc.dart +++ b/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_bloc.dart @@ -9,7 +9,7 @@ class EvaporasiSettingsBloc extends Bloc(_onStarted); on(_onThresholdRendahChanged); on(_onThresholdTinggiChanged); - on(_onRumusChanged); + // rumusKalibrasi removed: calculation is handled on ESP32 hardware on(_onOffsetChanged); on(_onPumpStartChanged); on(_onPumpEndChanged); @@ -40,12 +40,11 @@ class EvaporasiSettingsBloc extends Bloc.from(snap.value as Map); emit(state.copyWith( thresholdRendah: _toDouble(data['threshold_rendah'], 2.0), thresholdTinggi: _toDouble(data['threshold_tinggi'], 10.0), - rumusKalibrasi: (data['rumus_kalibrasi'] as String?) ?? 'selisih_max', koreksiOffset: _toDouble(data['koreksi_offset'], 0.0), pumpStartTime: (data['pump_start_time'] as String?) ?? (data['jam_pompa_mulai'] != null ? '${_toInt(data['jam_pompa_mulai'], 6).toString().padLeft(2, '0')}:00' : '06:00'), @@ -134,10 +133,7 @@ class EvaporasiSettingsBloc extends Bloc emit, ) => emit(state.copyWith(thresholdTinggi: event.value)); - void _onRumusChanged( - EvaporasiRumusKalibrasiChanged event, - Emitter emit, - ) => emit(state.copyWith(rumusKalibrasi: event.rumus)); + // rumusKalibrasi handling removed — device firmware defines the formula void _onOffsetChanged( EvaporasiKoreksiOffsetChanged event, @@ -221,17 +217,20 @@ class EvaporasiSettingsBloc extends Bloc get props => [value]; } -class EvaporasiRumusKalibrasiChanged extends EvaporasiSettingsEvent { - final String rumus; - const EvaporasiRumusKalibrasiChanged(this.rumus); - @override List get props => [rumus]; -} class EvaporasiKoreksiOffsetChanged extends EvaporasiSettingsEvent { final double value; 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 c5b0130..1514a37 100644 --- a/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_screen.dart +++ b/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_screen.dart @@ -444,148 +444,7 @@ class _StatusChip extends StatelessWidget { } } -class _RumusSelector extends StatelessWidget { - final String selected; - final ValueChanged onChanged; - const _RumusSelector({required this.selected, required this.onChanged}); - - @override - Widget build(BuildContext context) { - const options = [ - ( - value: 'selisih_max', - label: 'Selisih Maksimum', - subtitle: 'E = max(H kemarin) − max(H hari ini)', - icon: Icons.trending_down_rounded, - ), - ( - value: 'rata_harian', - label: 'Rata-rata Harian', - subtitle: 'E = rata(H kemarin) − rata(H hari ini)', - icon: Icons.bar_chart_rounded, - ), - ]; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Metode Kalkulasi', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.black54)), - const SizedBox(height: 10), - ...options.map((opt) { - final isSelected = opt.value == selected; - return GestureDetector( - onTap: () => onChanged(opt.value), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: isSelected ? Colors.blue.shade50 : Colors.grey.shade50, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: - isSelected ? Colors.blue.shade400 : Colors.grey.shade200, - width: isSelected ? 1.5 : 1, - ), - ), - child: Row( - children: [ - Icon(opt.icon, - color: isSelected - ? Colors.blue.shade700 - : Colors.grey.shade500, - size: 22), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(opt.label, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.bold, - color: isSelected - ? Colors.blue.shade800 - : Colors.black87)), - const SizedBox(height: 2), - Text(opt.subtitle, - style: TextStyle( - fontSize: 11, - fontFamily: 'monospace', - color: isSelected - ? Colors.blue.shade600 - : Colors.grey.shade500)), - ], - ), - ), - if (isSelected) - Icon(Icons.check_circle_rounded, - color: Colors.blue.shade600, size: 20), - ], - ), - ), - ); - }), - ], - ); - } -} - -class _FormulaPreview extends StatelessWidget { - final EvaporasiSettingsState state; - const _FormulaPreview({required this.state}); - - @override - Widget build(BuildContext context) { - final rumus = state.rumusKalibrasi == 'selisih_max' - ? 'E = max(H₁) − max(H₂) + offset' - : 'E = avg(H₁) − avg(H₂) + offset'; - final offsetStr = state.koreksiOffset >= 0 - ? '+${state.koreksiOffset.toStringAsFixed(2)}' - : state.koreksiOffset.toStringAsFixed(2); - - return Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [ - Icon(Icons.functions_rounded, - size: 15, color: Colors.amber.shade300), - const SizedBox(width: 8), - Text('Preview Rumus', - style: TextStyle( - fontSize: 12, - color: Colors.amber.shade300, - fontWeight: FontWeight.bold)), - ]), - const SizedBox(height: 10), - Text(rumus, - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade300, - fontFamily: 'monospace')), - const SizedBox(height: 6), - Text('offset = $offsetStr mm', - style: TextStyle( - fontSize: 12, - color: Colors.green.shade300, - fontFamily: 'monospace', - fontWeight: FontWeight.bold)), - ], - ), - ); - } -} - + class _InfoCard extends StatelessWidget { final String text; const _InfoCard(this.text); 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 3ad2281..0a1422b 100644 --- a/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_state.dart +++ b/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_state.dart @@ -5,7 +5,6 @@ enum EvaporasiSettingsStatus { loading, loaded, saving, saved, error } class EvaporasiSettingsState extends Equatable { final double thresholdRendah; final double thresholdTinggi; - final String rumusKalibrasi; final double koreksiOffset; final String pumpStartTime; final String pumpEndTime; @@ -45,7 +44,6 @@ class EvaporasiSettingsState extends Equatable { const EvaporasiSettingsState({ this.thresholdRendah = 2.0, this.thresholdTinggi = 10.0, - this.rumusKalibrasi = 'selisih_max', this.koreksiOffset = 0.0, this.pumpStartTime = '06:00', this.pumpEndTime = '18:00', @@ -82,7 +80,6 @@ class EvaporasiSettingsState extends Equatable { EvaporasiSettingsState copyWith({ double? thresholdRendah, double? thresholdTinggi, - String? rumusKalibrasi, double? koreksiOffset, String? pumpStartTime, String? pumpEndTime, @@ -118,7 +115,6 @@ class EvaporasiSettingsState extends Equatable { 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, @@ -155,7 +151,7 @@ class EvaporasiSettingsState extends Equatable { @override List get props => [ - thresholdRendah, thresholdTinggi, rumusKalibrasi, koreksiOffset, + thresholdRendah, thresholdTinggi, koreksiOffset, pumpStartTime, pumpEndTime, d0, dmaxManual, intervalRealtime_ms, intervalHistory_ms, intervalBaca_ms, standarTinggiCm, batasKritisCm, tempCompActive, tempCompCoef, tempRefC, diff --git a/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart b/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart index 110b08b..5293dcd 100644 --- a/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart +++ b/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart @@ -151,58 +151,6 @@ class EvaporasiBloc extends Bloc { _EvaporasiRealtimeUpdated event, Emitter emit, ) async { - final updatedHistory = List.from(state.history); - final dupIdx = updatedHistory.indexWhere( - (e) => e.timestamp.toUtc() == event.data.timestamp.toUtc(), - ); - if (dupIdx >= 0) { - updatedHistory[dupIdx] = event.data; - } else { - updatedHistory.add(event.data); - } - updatedHistory.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - - // Regenerasikan data chart secara dinamis agar selalu sinkron dengan data real-time - List updatedChart; - List updatedTemp; - List updatedLabels = state.chartLabels; - - final isSingle = _isSameDay(state.startDate, state.endDate); - - if (isSingle) { - updatedChart = TimeSeriesMapper.toSpecificDate( - data: updatedHistory, - getTime: (e) => e.timestamp, - getValue: (e) => e.evaporasi, - targetDate: state.startDate, - ); - updatedTemp = TimeSeriesMapper.toSpecificDate( - data: updatedHistory, - getTime: (e) => e.timestamp, - getValue: (e) => e.suhu, - targetDate: state.startDate, - ); - updatedLabels = List.generate(24, (i) => '${i.toString().padLeft(2, '0')}:00'); - } else { - final evapResult = TimeSeriesMapper.toDateRange( - data: updatedHistory, - getTime: (e) => e.timestamp, - getValue: (e) => e.evaporasi, - startDate: state.startDate, - endDate: state.endDate, - ); - final tempResult = TimeSeriesMapper.toDateRange( - data: updatedHistory, - getTime: (e) => e.timestamp, - getValue: (e) => e.suhu, - startDate: state.startDate, - endDate: state.endDate, - ); - updatedChart = evapResult.values; - updatedTemp = tempResult.values; - updatedLabels = evapResult.labels; - } - await _loadThresholdSettings(); final (status, willRain) = computeStatus( @@ -213,19 +161,9 @@ class EvaporasiBloc extends Bloc { _emitAlert(status, willRain, event.data.evaporasi); emit(state.copyWith( - history: updatedHistory, - filteredHistory: state.selectedDateFilter != null - ? updatedHistory.where((e) => - e.timestamp.year == state.selectedDateFilter!.year && - e.timestamp.month == state.selectedDateFilter!.month && - e.timestamp.day == state.selectedDateFilter!.day).toList() - : updatedHistory, currentValue: event.data.evaporasi, temperature: event.data.suhu, waterLevel: event.data.tinggiAir, - chartValues: updatedChart, - chartTemperatures: updatedTemp, - chartLabels: updatedLabels, weatherStatus: status, willRain: willRain, currentData: event.data, diff --git a/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart b/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart index c405d5d..b2f4d48 100644 --- a/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart +++ b/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart @@ -363,10 +363,6 @@ 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', diff --git a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_history_list.dart b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_history_list.dart index 331aa54..f9f29d6 100644 --- a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_history_list.dart +++ b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_history_list.dart @@ -34,7 +34,7 @@ class EvaporasiHistoryList extends StatelessWidget { Widget build(BuildContext context) { final grouped = _groupByDate(history); final sortedKeys = grouped.keys.toList() - ..sort((a, b) => b.compareTo(a)); // terbaru di atas + ..sort((a, b) => b.compareTo(a)); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -68,15 +68,11 @@ class EvaporasiHistoryList extends StatelessWidget { String _formatDateLabel(String key) { final dt = DateTime.parse(key); final today = DateTime.now(); - if (dt.year == today.year && - dt.month == today.month && - dt.day == today.day) { + if (dt.year == today.year && dt.month == today.month && dt.day == today.day) { return 'Hari Ini — ${DateFormat('dd MMMM yyyy', 'id_ID').format(dt)}'; } final yesterday = today.subtract(const Duration(days: 1)); - if (dt.year == yesterday.year && - dt.month == yesterday.month && - dt.day == yesterday.day) { + if (dt.year == yesterday.year && dt.month == yesterday.month && dt.day == yesterday.day) { return 'Kemarin — ${DateFormat('dd MMMM yyyy', 'id_ID').format(dt)}'; } return DateFormat('EEEE, dd MMMM yyyy', 'id_ID').format(dt); @@ -105,9 +101,12 @@ class _HeaderBar extends StatelessWidget { return Row( children: [ const Text( - 'History', + 'Riwayat Data', style: TextStyle( - fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87), + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), ), const Spacer(), if (filtered) @@ -171,37 +170,35 @@ class _ChipButton extends StatelessWidget { // ════════════════════════════════════════════════════════════ // Group per tanggal -// ════════════════════════════════════════════════════════════ -class _DateGroup extends StatelessWidget { +// ════════════════════════════════════════════════════ +class _DateGroup extends StatefulWidget { final String label; final List items; const _DateGroup({required this.label, required this.items}); - double get _avgEvap { - if (items.isEmpty) return 0; - return items.map((e) => e.evaporasi).reduce((a, b) => a + b) / items.length; - } + @override + State<_DateGroup> createState() => _DateGroupState(); +} - double get _maxEvap { - if (items.isEmpty) return 0; - return items.map((e) => e.evaporasi).reduce((a, b) => a > b ? a : b); +class _DateGroupState extends State<_DateGroup> { + bool _expanded = true; + + double get _avgEvap { + if (widget.items.isEmpty) return 0; + return widget.items.map((e) => e.evaporasi).reduce((a, b) => a + b) / + widget.items.length; } double get _avgTemp { - if (items.isEmpty) return 0; + if (widget.items.isEmpty) return 0; final validTemps = - items.where((e) => e.suhu >= -50 && e.suhu <= 100).toList(); + widget.items.where((e) => e.suhu >= -50 && e.suhu <= 100).toList(); if (validTemps.isEmpty) return 0; return validTemps.map((e) => e.suhu).reduce((a, b) => a + b) / validTemps.length; } - double get _maxTemp { - if (items.isEmpty) return 0; - return items.map((e) => e.suhu).reduce((a, b) => a > b ? a : b); - } - @override Widget build(BuildContext context) { return Container( @@ -217,41 +214,103 @@ class _DateGroup extends StatelessWidget { ), ], ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - Container( - width: 4, - height: 36, - decoration: BoxDecoration( - color: Colors.blue.shade600, - borderRadius: BorderRadius.circular(2), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Column( + children: [ + InkWell( + onTap: () => setState(() => _expanded = !_expanded), + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( children: [ - Text(label, - style: const TextStyle( - fontWeight: FontWeight.bold, fontSize: 13)), - const SizedBox(height: 2), - Text( - 'rata-rata ${_avgEvap.toStringAsFixed(2)} mm', - style: TextStyle(fontSize: 11, color: Colors.grey.shade600), + Container( + width: 4, + height: 36, + decoration: BoxDecoration( + color: Colors.blue.shade600, + borderRadius: BorderRadius.circular(2), + ), ), - const SizedBox(height: 2), - Text( - 'rata-rata suhu ${_avgTemp.toStringAsFixed(1)} °C', - style: TextStyle(fontSize: 11, color: Colors.grey.shade600), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.label, + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 13)), + const SizedBox(height: 2), + Text( + '${widget.items.length} data • rata-rata ${_avgEvap.toStringAsFixed(2)} mm • suhu rata-rata ${_avgTemp.toStringAsFixed(1)} °C', + style: TextStyle(fontSize: 11, color: Colors.grey.shade600), + ), + ], + ), + ), + Icon( + _expanded + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, + color: Colors.grey.shade500, ), ], ), ), - ], - ), + ), + if (_expanded) + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: widget.items.length, + separatorBuilder: (_, __) => Divider(height: 1, color: Colors.grey.shade100), + itemBuilder: (context, i) => _HistoryItemTile(item: widget.items[i]), + ), + ], + ), + ); + } +} + +class _HistoryItemTile extends StatelessWidget { + final Evaporasi item; + + const _HistoryItemTile({required this.item}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + children: [ + SizedBox( + width: 52, + child: Text( + DateFormat('HH:mm:ss').format(item.timestamp), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + fontFamily: 'monospace', + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${item.evaporasi.toStringAsFixed(2)} mm • ${item.suhu.toStringAsFixed(1)} °C • ${item.tinggiAir.toStringAsFixed(1)} cm', + style: const TextStyle(fontSize: 13, color: Colors.black87), + ), + const SizedBox(height: 4), + Text( + item.status, + style: TextStyle(fontSize: 11, color: Colors.grey.shade600), + ), + ], + ), + ), + ], ), ); }