From f9abec81c349516f6c81165440e8675c7e087d9f Mon Sep 17 00:00:00 2001 From: Mochamad ongki ramadani Date: Sat, 9 May 2026 18:00:57 +0700 Subject: [PATCH] percobaan --- .../evaporasi/blocs/evaporasi_bloc.dart | 112 +++++---- .../evaporasi/blocs/evaporasi_state.dart | 35 ++- .../evaporasi/views/evaporasi_screen.dart | 224 ++++++++++++------ .../views/widgets/evaporasi_chart_widget.dart | 46 +++- .../widgets/evaporasi_date_search_bar.dart | 72 ++++++ .../lib/src/models/evaporasi.dart | 9 +- 6 files changed, 357 insertions(+), 141 deletions(-) create mode 100644 lib/screens/monitoring/evaporasi/views/widgets/evaporasi_date_search_bar.dart diff --git a/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart b/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart index 011dd9f..1f3a593 100644 --- a/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart +++ b/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart @@ -27,29 +27,24 @@ class EvaporasiBloc extends Bloc { on(_onViewModeChanged); } - /// ========================= - /// ๐Ÿš€ START - /// ========================= Future _onStarted( WatchEvaporasiStarted event, Emitter emit, ) async { - // chartLabels & listData untuk list/label di UI - emit(state.copyWith(isLoading: true)); + // Ambil history yang akan dipakai untuk list + agregasi grafik. final history = await _repository.getSensorHistory( - 'evaporasi/history', + 'Monitoring/History', (json) => Evaporasi.fromJson(json), + orderByChild: null, + limit: 500, ); - // listData dipakai untuk tab/list di UI (default: ambil data yang sudah ada di Firebase) final listData = List.from(history) ..sort((a, b) => a.timestamp.compareTo(b.timestamp)); - final dailyGraph = TimeSeriesMapper.toDaily( - data: history, getTime: (e) => e.timestamp, getValue: (e) => e.evaporasi, @@ -61,7 +56,30 @@ class EvaporasiBloc extends Bloc { getValue: (e) => e.suhu, ); - // Hitung status dari data terakhir history jika ada + final weeklyGraph = TimeSeriesMapper.toWeekly( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.evaporasi, + ); + + final monthlyGraph = TimeSeriesMapper.toMonthly( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.evaporasi, + ); + + final weeklyTemp = TimeSeriesMapper.toWeekly( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.suhu, + ); + + final monthlyTemp = TimeSeriesMapper.toMonthly( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.suhu, + ); + final lastValue = history.isNotEmpty ? history.last.evaporasi : 0.0; final (status, rain) = _computeWeatherStatus(lastValue); _emitEvaporasiAlert(status, rain, lastValue); @@ -71,9 +89,11 @@ class EvaporasiBloc extends Bloc { listData: listData, dailyValues: dailyGraph, dailyTemperatures: dailyTempGraph, - chartLabels: _buildChartLabels( - period: 'Hari Ini', - ), + weeklyValues: weeklyGraph, + monthlyValues: monthlyGraph, + weeklyTemperatures: weeklyTemp, + monthlyTemperatures: monthlyTemp, + chartLabels: _buildChartLabels(period: 'Hari Ini'), weatherStatus: status, willRain: rain, isLoading: false, @@ -85,18 +105,16 @@ class EvaporasiBloc extends Bloc { .listen((data) => add(_EvaporasiRealtimeUpdated(data))); } - /// ========================= - /// โšก REALTIME - /// ========================= void _onRealtimeUpdated( _EvaporasiRealtimeUpdated event, Emitter emit, ) { + // Hanya update grafik daily (index jam saat ini). final updated = List.from(state.dailyValues); final updatedTemp = List.from(state.dailyTemperatures); final index = DateTime.now().hour; - if (index < updated.length) { + if (index >= 0 && index < updated.length) { updated[index] = event.data.evaporasi; updatedTemp[index] = event.data.suhu; } @@ -115,20 +133,18 @@ class EvaporasiBloc extends Bloc { )); } - /// ========================= - /// ๐Ÿ“Š PERIOD - /// ========================= Future _onPeriodChanged( EvaporasiPeriodChanged event, Emitter emit, ) async { emit(state.copyWith(isLoading: true, selectedPeriod: event.period)); + final history = state.history; List updated; List updatedTemp; - if (event.period == "Minggu Ini") { + if (event.period == 'Minggu Ini') { updated = TimeSeriesMapper.toWeekly( data: history, getTime: (e) => e.timestamp, @@ -139,7 +155,7 @@ class EvaporasiBloc extends Bloc { getTime: (e) => e.timestamp, getValue: (e) => e.suhu, ); - } else if (event.period == "Bulan Ini") { + } else if (event.period == 'Bulan Ini') { updated = TimeSeriesMapper.toMonthly( data: history, getTime: (e) => e.timestamp, @@ -163,18 +179,17 @@ class EvaporasiBloc extends Bloc { ); } -// Pertahankan status cuaca saat ini (tidak berubah karena hanya ganti periode) emit(state.copyWith( dailyValues: updated, dailyTemperatures: updatedTemp, chartLabels: _buildChartLabels(period: event.period), isLoading: false, + // penting: jangan hilangkan list + listData: state.listData, + history: state.history, )); } - /// ========================= - /// ๐Ÿ“… DATE SELECTED (Custom Date) - /// ========================= Future _onDateSelected( EvaporasiDateSelected event, Emitter emit, @@ -206,18 +221,17 @@ class EvaporasiBloc extends Bloc { dailyTemperatures: updatedTemp, chartLabels: _buildChartLabels(period: 'Tanggal Khusus'), isLoading: false, + // jangan hilangkan list + listData: state.listData, + history: state.history, )); } - /// ========================= - /// ๐Ÿ”„ VIEW MODE CHANGED - /// ========================= Future _onViewModeChanged( EvaporasiViewModeChanged event, Emitter emit, ) async { if (event.mode == EvaporasiViewMode.period) { - // Kembali ke mode period - trigger period change add(EvaporasiPeriodChanged(state.selectedPeriod)); } else { emit(state.copyWith(viewMode: event.mode)); @@ -230,31 +244,12 @@ class EvaporasiBloc extends Bloc { return super.close(); } - /// ========================= - /// ๐ŸŒค๏ธ HELPER: Status Cuaca - /// ========================= static (String status, bool willRain) _computeWeatherStatus(double value) { if (value <= 5.0) return ('Baik', false); if (value <= 10.0) return ('Sedang', true); return ('Buruk', true); } - List _buildChartLabels({required String period}) { - if (period == 'Minggu Ini') { - return const ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min']; - } - - // Untuk Bulan Ini & Tanggal Khusus/ Hari Ini, biarkan chart pakai 0..23 / 0..days - final now = DateTime.now(); - if (period == 'Bulan Ini') { - final daysInMonth = DateTime(now.year, now.month + 1, 0).day; - return List.generate(daysInMonth, (i) => '${i + 1}'); - } - - // Hari Ini / Tanggal Khusus => 24 jam - return List.generate(24, (i) => '${i.toString().padLeft(2, '0')}:00'); - } - void _emitEvaporasiAlert(String status, bool willRain, double value) { final AlertSeverity severity; final String message; @@ -268,7 +263,7 @@ class EvaporasiBloc extends Bloc { message = 'Evaporasi ${value.toStringAsFixed(1)} mm โ€” status sedang, potensi hujan'; } else { - severity = AlertSeverity.info; // normal โ†’ bersihkan alert + severity = AlertSeverity.info; message = ''; } @@ -282,9 +277,23 @@ class EvaporasiBloc extends Bloc { ), )); } + + List _buildChartLabels({required String period}) { + if (period == 'Minggu Ini') { + return const ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min']; + } + + final now = DateTime.now(); + if (period == 'Bulan Ini') { + final daysInMonth = DateTime(now.year, now.month + 1, 0).day; + return List.generate(daysInMonth, (i) => '${i + 1}'); + } + + // Hari Ini / Tanggal Khusus => 24 jam + return List.generate(24, (i) => '${i.toString().padLeft(2, '0')}:00'); + } } -/// INTERNAL EVENT class _EvaporasiRealtimeUpdated extends EvaporasiEvent { final Evaporasi data; @@ -293,3 +302,4 @@ class _EvaporasiRealtimeUpdated extends EvaporasiEvent { @override List get props => [data]; } + diff --git a/lib/screens/monitoring/evaporasi/blocs/evaporasi_state.dart b/lib/screens/monitoring/evaporasi/blocs/evaporasi_state.dart index fcbd8cf..0486273 100644 --- a/lib/screens/monitoring/evaporasi/blocs/evaporasi_state.dart +++ b/lib/screens/monitoring/evaporasi/blocs/evaporasi_state.dart @@ -10,8 +10,13 @@ class EvaporasiState extends Equatable { final DateTime? selectedDate; // tanggal spesifik untuk custom date final EvaporasiViewMode viewMode; // period vs custom date - final List dailyValues; // untuk grafik evaporasi + final List dailyValues; // untuk grafik evaporasi (default) + final List weeklyValues; + final List monthlyValues; + final List dailyTemperatures; // untuk grafik suhu + final List weeklyTemperatures; + final List monthlyTemperatures; /// Label X-axis sesuai agregasi period /// Contoh: Hari Ini => ["00:00","01:00",...] @@ -21,6 +26,7 @@ class EvaporasiState extends Equatable { /// Data untuk list (timestamp asli dari firebase) final List listData; + final List history; final String weatherStatus; // Baik / Sedang / Buruk @@ -32,20 +38,24 @@ class EvaporasiState extends Equatable { this.currentValue = 0.0, this.temperature = 0.0, this.waterLevel = 0.0, - this.selectedPeriod = "Hari Ini", + this.selectedPeriod = 'Hari Ini', this.selectedDate, this.viewMode = EvaporasiViewMode.period, this.dailyValues = const [], + this.weeklyValues = const [], + this.monthlyValues = const [], this.dailyTemperatures = const [], + this.weeklyTemperatures = const [], + this.monthlyTemperatures = const [], this.chartLabels = const [], this.listData = const [], this.history = const [], - this.weatherStatus = "Baik", + this.weatherStatus = 'Baik', this.willRain = false, this.isLoading = true, }); -EvaporasiState copyWith({ + EvaporasiState copyWith({ double? currentValue, double? temperature, double? waterLevel, @@ -53,7 +63,11 @@ EvaporasiState copyWith({ DateTime? selectedDate, EvaporasiViewMode? viewMode, List? dailyValues, + List? weeklyValues, + List? monthlyValues, List? dailyTemperatures, + List? weeklyTemperatures, + List? monthlyTemperatures, List? chartLabels, List? listData, List? history, @@ -69,10 +83,14 @@ EvaporasiState copyWith({ selectedDate: selectedDate ?? this.selectedDate, viewMode: viewMode ?? this.viewMode, dailyValues: dailyValues ?? this.dailyValues, + weeklyValues: weeklyValues ?? this.weeklyValues, + monthlyValues: monthlyValues ?? this.monthlyValues, dailyTemperatures: dailyTemperatures ?? this.dailyTemperatures, - history: history ?? this.history, + weeklyTemperatures: weeklyTemperatures ?? this.weeklyTemperatures, + monthlyTemperatures: monthlyTemperatures ?? this.monthlyTemperatures, chartLabels: chartLabels ?? this.chartLabels, listData: listData ?? this.listData, + history: history ?? this.history, weatherStatus: weatherStatus ?? this.weatherStatus, willRain: willRain ?? this.willRain, isLoading: isLoading ?? this.isLoading, @@ -88,10 +106,17 @@ EvaporasiState copyWith({ selectedDate, viewMode, dailyValues, + weeklyValues, + monthlyValues, dailyTemperatures, + weeklyTemperatures, + monthlyTemperatures, + chartLabels, + listData, history, weatherStatus, willRain, isLoading, ]; } + diff --git a/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart b/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart index e1ea08c..2b7e8b5 100644 --- a/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart +++ b/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart @@ -1,15 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; + import '../blocs/evaporasi_bloc.dart'; import 'widgets/evaporasi_chart_widget.dart'; import 'widgets/evaporasi_period_selector.dart'; import '../../shared/utils/pdf/pdf_export_service.dart'; import '../../shared/widgets/export_pdf_button.dart'; -class EvaporasiScreen extends StatelessWidget { +class EvaporasiScreen extends StatefulWidget { const EvaporasiScreen({super.key}); + @override + State createState() => _EvaporasiScreenState(); +} + +class _EvaporasiScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( @@ -49,9 +55,10 @@ class EvaporasiScreen extends StatelessWidget { const SizedBox(height: 25), _statusCard(state), const SizedBox(height: 25), - const Text("Tren Evaporasi & Suhu", - style: - TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Text( + "Tren Evaporasi & Suhu", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), const SizedBox(height: 15), // Period selector with date picker @@ -61,7 +68,6 @@ class EvaporasiScreen extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Show selected date info when in custom mode if (state.viewMode == EvaporasiViewMode.customDate && state.selectedDate != null) Padding( @@ -80,28 +86,32 @@ class EvaporasiScreen extends StatelessWidget { ); }, ), + const SizedBox(height: 15), + + // Chart Builder( builder: (context) { final state = context.watch().state; - return Column( - children: [ - EvaporasiChartWidget( - dailyValues: state.dailyValues, - dailyTemperatures: state.dailyTemperatures, - period: state.viewMode == EvaporasiViewMode.customDate - ? "Tanggal Khusus" - : state.selectedPeriod, - chartLabels: state.chartLabels, - ), - const SizedBox(height: 20), - _evaporasiList(state), - const SizedBox(height: 10), - ], + return EvaporasiChartWidget( + dailyValues: state.dailyValues, + dailyTemperatures: state.dailyTemperatures, + period: state.viewMode == EvaporasiViewMode.customDate + ? "Tanggal Khusus" + : state.selectedPeriod, + chartLabels: state.chartLabels, ); }, ), + + const SizedBox(height: 20), + + // List data (mengikuti chart: period vs custom date) + _evaporasiList(state), + + const SizedBox(height: 10), + ExportPdfButton( onExport: () => PdfExportService.evaporasi( evaporasi: state.currentValue, @@ -224,6 +234,20 @@ class EvaporasiScreen extends StatelessWidget { break; } + // Teks peringatan khusus evaporasi (normal/sedang/tinggi) + final String warningText; + if (state.weatherStatus == 'Baik') { + warningText = + 'Normal โ€” evaporasi stabil, risiko dampak rendah.'; + } else if (state.weatherStatus == 'Sedang') { + warningText = + 'Sedang โ€” evaporasi mulai tinggi, pantau kondisi cuaca.'; + } else { + warningText = + 'Tinggi โ€” evaporasi signifikan, berpotensi memengaruhi kondisi lingkungan.'; + } + + return Container( width: double.infinity, padding: const EdgeInsets.all(20), @@ -246,26 +270,32 @@ class EvaporasiScreen extends StatelessWidget { ), const SizedBox(height: 4), Text( - state.weatherStatus, + state.weatherStatus == 'Baik' + ? 'Normal' + : state.weatherStatus == 'Sedang' + ? 'Sedang' + : 'Tinggi', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: statusColor, ), ), + if (state.willRain) - const Text( - "โš ๏ธ Potensi hujan tinggi", - style: TextStyle( + Text( + warningText, + style: const TextStyle( fontSize: 12, color: Colors.red, fontWeight: FontWeight.w500, ), ), + ], ), ), -], + ], ), ); } @@ -274,7 +304,15 @@ class EvaporasiScreen extends StatelessWidget { /// ๐Ÿงพ LIST DATA EVAPORASI /// ========================= Widget _evaporasiList(EvaporasiState state) { - final data = state.listData; + final data = state.viewMode == EvaporasiViewMode.customDate && state.selectedDate != null + ? state.history + .where((e) => + e.timestamp.year == state.selectedDate!.year && + e.timestamp.month == state.selectedDate!.month && + e.timestamp.day == state.selectedDate!.day) + .toList() + : state.history; + if (data.isEmpty) { return Container( width: double.infinity, @@ -283,8 +321,10 @@ class EvaporasiScreen extends StatelessWidget { color: Colors.white, borderRadius: BorderRadius.circular(20), ), - child: const Text('Belum ada data evaporasi', - style: TextStyle(color: Colors.grey)), + child: const Text( + 'Belum ada data evaporasi', + style: TextStyle(color: Colors.grey), + ), ); } @@ -303,60 +343,77 @@ class EvaporasiScreen extends StatelessWidget { style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), ), const SizedBox(height: 12), - ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: data.length, - separatorBuilder: (_, __) => const Divider(height: 1), - itemBuilder: (context, index) { - final e = data[index]; - final dateLabel = DateFormat('dd MMM yyyy', 'id_ID') - .format(e.timestamp) + - ' โ€ข ' + - DateFormat('HH:mm:ss', 'id_ID').format(e.timestamp); + SizedBox( + height: 280, + child: ListView.separated( + itemCount: data.length, + itemBuilder: (context, index) { + final e = data[index]; + final dateLabel = + '${DateFormat('dd MMM yyyy', 'id_ID').format(e.timestamp)} โ€ข ${DateFormat('HH:mm:ss', 'id_ID').format(e.timestamp)}'; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - dateLabel, - style: const TextStyle( - fontSize: 12, - color: Colors.black87, - ), - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '${e.evaporasi.toStringAsFixed(1)} mm', + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + dateLabel, style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w700, - color: Colors.blue, - ), - ), - const SizedBox(height: 4), - Text( - '${e.suhu.toStringAsFixed(1)} ยฐC', - style: TextStyle( fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.orange.shade700, + color: Colors.black87, ), + overflow: TextOverflow.ellipsis, ), - ], - ), - ], - ), - ); - }, + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${e.evaporasi.toStringAsFixed(1)} mm', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: Colors.blue, + ), + ), + const SizedBox(height: 4), + Text( + 'Tinggi Air: ${e.tinggiAir.toStringAsFixed(1)} cm', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.blue.shade700, + ), + ), + const SizedBox(height: 4), + Text( + 'Suhu: ${e.suhu.toStringAsFixed(1)} ยฐC', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.orange.shade700, + ), + ), + const SizedBox(height: 4), + Text( + _statusTextForHistory(e.evaporasi), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: _statusColorForHistory(e.evaporasi), + ), + ), + ], + ), + ], + ), + ); + }, + separatorBuilder: (_, __) => const Divider(height: 1), + ), ), ], ), @@ -366,8 +423,22 @@ class EvaporasiScreen extends StatelessWidget { /// ========================= /// ๐Ÿ“… FORMAT DATE INFO (for custom date display) /// ========================= + String _statusTextForHistory(double evaporasi) { + if (evaporasi <= 5.0) return 'Status: Normal'; + if (evaporasi <= 10.0) return 'Status: Sedang'; + return 'Status: Tinggi'; + } + + Color _statusColorForHistory(double evaporasi) { + if (evaporasi <= 5.0) return Colors.green; + if (evaporasi <= 10.0) return Colors.orange; + return Colors.red; + } + String _formatDateInfo(DateTime date) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); final yesterday = today.subtract(const Duration(days: 1)); final selected = DateTime(date.year, date.month, date.day); @@ -381,3 +452,4 @@ class EvaporasiScreen extends StatelessWidget { } } } + diff --git a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_chart_widget.dart b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_chart_widget.dart index af307b4..8b6a8a7 100644 --- a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_chart_widget.dart +++ b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_chart_widget.dart @@ -72,18 +72,42 @@ class EvaporasiChartWidget extends StatelessWidget { duration: const Duration(milliseconds: 600), curve: Curves.easeOutCubic, LineChartData( + minY: 0, maxY: maxY, gridData: const FlGridData(show: false), borderData: FlBorderData(show: false), titlesData: FlTitlesData( show: true, - rightTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false)), + // dual axis: kiri untuk Evaporasi (mm), kanan untuk Suhu (ยฐC) + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + interval: _getYAxisInterval(safeValues), + getTitlesWidget: (value, meta) { + return Text( + value.toStringAsFixed(0), + style: const TextStyle(color: Colors.blueGrey, fontSize: 10), + ); + }, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + interval: _getYAxisInterval(safeTemps), + getTitlesWidget: (value, meta) { + return Text( + value.toStringAsFixed(0), + style: const TextStyle(color: Colors.orangeAccent, fontSize: 10), + ); + }, + ), + ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), - leftTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, @@ -105,7 +129,7 @@ class EvaporasiChartWidget extends StatelessWidget { ), ), lineBarsData: [ - // === Garis Evaporasi === + // === Garis Evaporasi (kiri axis) === LineChartBarData( spots: safeValues.asMap().entries.map((e) { return FlSpot(e.key.toDouble(), e.value); @@ -143,7 +167,7 @@ class EvaporasiChartWidget extends StatelessWidget { ), ), ), - // === Garis Suhu === + // === Garis Suhu (kanan axis) === LineChartBarData( spots: safeTemps.asMap().entries.map((e) { return FlSpot(e.key.toDouble(), e.value); @@ -245,5 +269,15 @@ class EvaporasiChartWidget extends StatelessWidget { if (period == "Minggu Ini") return 1; return 1; } + + double _getYAxisInterval(List data) { + if (data.isEmpty) return 1; + final max = data.reduce((a, b) => a > b ? a : b); + if (max <= 10) return 2; + if (max <= 20) return 5; + if (max <= 30) return 10; + return 20; + } } + diff --git a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_date_search_bar.dart b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_date_search_bar.dart new file mode 100644 index 0000000..bf29d28 --- /dev/null +++ b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_date_search_bar.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import 'package:monitoring_repository/monitoring_repository.dart'; + +class EvaporasiDateSearchBar extends StatefulWidget { + final String initialQuery; + final ValueChanged onQueryChanged; + + const EvaporasiDateSearchBar({ + super.key, + required this.initialQuery, + required this.onQueryChanged, + }); + + + @override + State createState() => _EvaporasiDateSearchBarState(); +} + +class _EvaporasiDateSearchBarState extends State { + late final TextEditingController _controller; + DateTime? _selected; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.initialQuery); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + bool _matches(Evaporasi item, String query) { + if (query.trim().isEmpty) return true; + + final date = item.timestamp; + final ddMMyyyy = DateFormat('dd/MM/yyyy', 'id_ID').format(date); + final ddMMMYYYY = DateFormat('dd MMM yyyy', 'id_ID').format(date); + + final q = query.trim().toLowerCase(); + return ddMMyyyy.toLowerCase().contains(q) || ddMMMYYYY.toLowerCase().contains(q); + } + + @override + Widget build(BuildContext context) { + return TextField( + + controller: _controller, + decoration: InputDecoration( + hintText: 'Cari tanggal (dd/MM/yyyy)', + prefixIcon: const Icon(Icons.search), + suffixIcon: _controller.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () => setState(() => _controller.clear()), + ) + : null, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), + isDense: true, + ), + onChanged: (v) { + widget.onQueryChanged(v); + setState(() {}); + }, + ); + } +} + diff --git a/packages/monitoring_repository/lib/src/models/evaporasi.dart b/packages/monitoring_repository/lib/src/models/evaporasi.dart index caec82e..fc706ae 100644 --- a/packages/monitoring_repository/lib/src/models/evaporasi.dart +++ b/packages/monitoring_repository/lib/src/models/evaporasi.dart @@ -32,9 +32,12 @@ class Evaporasi implements HasTimestamp { final suhu = (json['suhu'] ?? json['suhu_air'] ?? 0).toDouble(); final tinggi = (json['tinggi'] ?? json['tinggi_air'] ?? 0).toDouble(); - // โœ… Prioritas: timestamp (Unix, kalau firmware sudah diupdate) - // โ†’ fallback ke waktu ("HH:MM:SS") - final rawTime = json['timestamp'] ?? json['waktu']; + // โœ… Prioritas waktu sesuai firmware ESP32: + // 1) timestamp (Unix, kalau pernah dikirim) + // 2) datetime ("YYYY-MM-DD HH:MM:SS") + // 3) waktu ("HH:MM:SS") fallback (akan memakai tanggal hari ini) + final rawTime = json['timestamp'] ?? json['datetime'] ?? json['waktu']; + return Evaporasi( evaporasi: (json['evaporasi'] ?? 0).toDouble(),