diff --git a/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart b/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart index 32cee33..011dd9f 100644 --- a/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart +++ b/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart @@ -34,6 +34,8 @@ class EvaporasiBloc extends Bloc { WatchEvaporasiStarted event, Emitter emit, ) async { + // chartLabels & listData untuk list/label di UI + emit(state.copyWith(isLoading: true)); final history = await _repository.getSensorHistory( @@ -41,7 +43,13 @@ class EvaporasiBloc extends Bloc { (json) => Evaporasi.fromJson(json), ); + // 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, @@ -60,8 +68,12 @@ class EvaporasiBloc extends Bloc { emit(state.copyWith( history: history, + listData: listData, dailyValues: dailyGraph, dailyTemperatures: dailyTempGraph, + chartLabels: _buildChartLabels( + period: 'Hari Ini', + ), weatherStatus: status, willRain: rain, isLoading: false, @@ -155,6 +167,7 @@ class EvaporasiBloc extends Bloc { emit(state.copyWith( dailyValues: updated, dailyTemperatures: updatedTemp, + chartLabels: _buildChartLabels(period: event.period), isLoading: false, )); } @@ -191,6 +204,7 @@ class EvaporasiBloc extends Bloc { emit(state.copyWith( dailyValues: updated, dailyTemperatures: updatedTemp, + chartLabels: _buildChartLabels(period: 'Tanggal Khusus'), isLoading: false, )); } @@ -225,6 +239,22 @@ class EvaporasiBloc extends Bloc { 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; diff --git a/lib/screens/monitoring/evaporasi/blocs/evaporasi_state.dart b/lib/screens/monitoring/evaporasi/blocs/evaporasi_state.dart index 640d2dd..fcbd8cf 100644 --- a/lib/screens/monitoring/evaporasi/blocs/evaporasi_state.dart +++ b/lib/screens/monitoring/evaporasi/blocs/evaporasi_state.dart @@ -12,6 +12,15 @@ class EvaporasiState extends Equatable { final List dailyValues; // untuk grafik evaporasi final List dailyTemperatures; // untuk grafik suhu + + /// Label X-axis sesuai agregasi period + /// Contoh: Hari Ini => ["00:00","01:00",...] + /// Minggu Ini => ["Sen","Sel",...] + /// Bulan Ini => ["1","2",...] + final List chartLabels; + + /// Data untuk list (timestamp asli dari firebase) + final List listData; final List history; final String weatherStatus; // Baik / Sedang / Buruk @@ -28,6 +37,8 @@ class EvaporasiState extends Equatable { this.viewMode = EvaporasiViewMode.period, this.dailyValues = const [], this.dailyTemperatures = const [], + this.chartLabels = const [], + this.listData = const [], this.history = const [], this.weatherStatus = "Baik", this.willRain = false, @@ -43,6 +54,8 @@ EvaporasiState copyWith({ EvaporasiViewMode? viewMode, List? dailyValues, List? dailyTemperatures, + List? chartLabels, + List? listData, List? history, String? weatherStatus, bool? willRain, @@ -58,6 +71,8 @@ EvaporasiState copyWith({ dailyValues: dailyValues ?? this.dailyValues, dailyTemperatures: dailyTemperatures ?? this.dailyTemperatures, history: history ?? this.history, + chartLabels: chartLabels ?? this.chartLabels, + listData: listData ?? this.listData, weatherStatus: weatherStatus ?? this.weatherStatus, willRain: willRain ?? this.willRain, isLoading: isLoading ?? this.isLoading, diff --git a/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart b/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart index eb3343a..cc403a7 100644 --- a/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart +++ b/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart @@ -53,7 +53,8 @@ class EvaporasiScreen extends StatelessWidget { style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 15), -// Period selector with date picker + + // Period selector with date picker Builder( builder: (context) { final state = context.watch().state; @@ -83,16 +84,24 @@ class EvaporasiScreen extends StatelessWidget { Builder( builder: (context) { final state = context.watch().state; - return EvaporasiChartWidget( - dailyValues: state.dailyValues, - dailyTemperatures: state.dailyTemperatures, - period: state.viewMode == EvaporasiViewMode.customDate - ? "Tanggal Khusus" - : state.selectedPeriod, + 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), + ], ); }, ), - const SizedBox(height: 25), + const SizedBox(height: 10), ExportPdfButton( onExport: () => PdfExportService.evaporasi( evaporasi: state.currentValue, @@ -261,6 +270,82 @@ class EvaporasiScreen extends StatelessWidget { ); } + /// ========================= + /// ๐Ÿงพ LIST DATA EVAPORASI + /// ========================= + Widget _evaporasiList(EvaporasiState state) { + final data = state.listData; + if (data.isEmpty) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: const Text('Belum ada data evaporasi', + style: TextStyle(color: Colors.grey)), + ); + } + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'List Data Evaporasi', + 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 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), + Text( + '${e.evaporasi.toStringAsFixed(1)} mm', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: Colors.blue, + ), + ), + ], + ), + ); + }, + ), + ], + ), + ); + } + /// ========================= /// ๐Ÿ“… FORMAT DATE INFO (for custom date display) /// ========================= 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 fe0a686..af307b4 100644 --- a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_chart_widget.dart +++ b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_chart_widget.dart @@ -5,14 +5,18 @@ class EvaporasiChartWidget extends StatelessWidget { final List dailyValues; final List dailyTemperatures; final String period; + final List chartLabels; + const EvaporasiChartWidget({ super.key, required this.dailyValues, required this.dailyTemperatures, required this.period, + required this.chartLabels, }); + double _getMaxY(List data) { if (data.isEmpty) return 10; final max = data.reduce((a, b) => a > b ? a : b); @@ -93,6 +97,7 @@ class EvaporasiChartWidget extends StatelessWidget { _getBottomTitle(value), style: const TextStyle( color: Colors.grey, fontSize: 10), + ), ); }, @@ -227,20 +232,14 @@ class EvaporasiChartWidget extends StatelessWidget { } String _getBottomTitle(double value) { - int index = value.toInt(); - final len = dailyValues.length; - if (index < 0 || index >= len) return ''; - - if (period == "Hari Ini") { - return index % 4 == 0 ? "${index.toString().padLeft(2, '0')}:00" : ""; - } else if (period == "Minggu Ini") { - const days = ["Sen", "Sel", "Rab", "Kam", "Jum", "Sab", "Min"]; - return days[index % 7]; - } else { - return (index + 1) % 5 == 0 ? "${index + 1}" : ""; - } + final index = value.toInt(); + if (chartLabels.isEmpty) return ''; + if (index < 0 || index >= chartLabels.length) return ''; + return chartLabels[index]; } + + double _getInterval() { if (period == "Hari Ini") return 1; if (period == "Minggu Ini") return 1; diff --git a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_period_selector.dart b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_period_selector.dart index ee5ea72..45b79ff 100644 --- a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_period_selector.dart +++ b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_period_selector.dart @@ -68,7 +68,20 @@ Widget _buildDatePickerButton(BuildContext context, EvaporasiViewMode viewMode, context.read().add( const EvaporasiViewModeChanged(EvaporasiViewMode.customDate), ); - showEvaporasiDatePicker(context); + + // Penting: pastikan bottom sheet masih punya context yang memiliki EvaporasiBloc + final bloc = context.read(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (sheetContext) { + return BlocProvider.value( + value: bloc, + child: const EvaporasiDatePicker(), + ); + }, + ); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),