Fix evaporasi dashboard: firebase history list + sync chart labels

This commit is contained in:
Mochamad ongki ramadani 2026-05-08 12:36:45 +07:00
parent 6ef9ab0985
commit 42bac0c980
5 changed files with 163 additions and 21 deletions

View File

@ -34,6 +34,8 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
WatchEvaporasiStarted event, WatchEvaporasiStarted event,
Emitter<EvaporasiState> emit, Emitter<EvaporasiState> emit,
) async { ) async {
// chartLabels & listData untuk list/label di UI
emit(state.copyWith(isLoading: true)); emit(state.copyWith(isLoading: true));
final history = await _repository.getSensorHistory( final history = await _repository.getSensorHistory(
@ -41,7 +43,13 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
(json) => Evaporasi.fromJson(json), (json) => Evaporasi.fromJson(json),
); );
// listData dipakai untuk tab/list di UI (default: ambil data yang sudah ada di Firebase)
final listData = List<Evaporasi>.from(history)
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
final dailyGraph = TimeSeriesMapper.toDaily( final dailyGraph = TimeSeriesMapper.toDaily(
data: history, data: history,
getTime: (e) => e.timestamp, getTime: (e) => e.timestamp,
getValue: (e) => e.evaporasi, getValue: (e) => e.evaporasi,
@ -60,8 +68,12 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
emit(state.copyWith( emit(state.copyWith(
history: history, history: history,
listData: listData,
dailyValues: dailyGraph, dailyValues: dailyGraph,
dailyTemperatures: dailyTempGraph, dailyTemperatures: dailyTempGraph,
chartLabels: _buildChartLabels(
period: 'Hari Ini',
),
weatherStatus: status, weatherStatus: status,
willRain: rain, willRain: rain,
isLoading: false, isLoading: false,
@ -155,6 +167,7 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
emit(state.copyWith( emit(state.copyWith(
dailyValues: updated, dailyValues: updated,
dailyTemperatures: updatedTemp, dailyTemperatures: updatedTemp,
chartLabels: _buildChartLabels(period: event.period),
isLoading: false, isLoading: false,
)); ));
} }
@ -191,6 +204,7 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
emit(state.copyWith( emit(state.copyWith(
dailyValues: updated, dailyValues: updated,
dailyTemperatures: updatedTemp, dailyTemperatures: updatedTemp,
chartLabels: _buildChartLabels(period: 'Tanggal Khusus'),
isLoading: false, isLoading: false,
)); ));
} }
@ -225,6 +239,22 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
return ('Buruk', true); return ('Buruk', true);
} }
List<String> _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) { void _emitEvaporasiAlert(String status, bool willRain, double value) {
final AlertSeverity severity; final AlertSeverity severity;
final String message; final String message;

View File

@ -12,6 +12,15 @@ class EvaporasiState extends Equatable {
final List<double> dailyValues; // untuk grafik evaporasi final List<double> dailyValues; // untuk grafik evaporasi
final List<double> dailyTemperatures; // untuk grafik suhu final List<double> 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<String> chartLabels;
/// Data untuk list (timestamp asli dari firebase)
final List<Evaporasi> listData;
final List<Evaporasi> history; final List<Evaporasi> history;
final String weatherStatus; // Baik / Sedang / Buruk final String weatherStatus; // Baik / Sedang / Buruk
@ -28,6 +37,8 @@ class EvaporasiState extends Equatable {
this.viewMode = EvaporasiViewMode.period, this.viewMode = EvaporasiViewMode.period,
this.dailyValues = const [], this.dailyValues = const [],
this.dailyTemperatures = const [], this.dailyTemperatures = const [],
this.chartLabels = const [],
this.listData = const [],
this.history = const [], this.history = const [],
this.weatherStatus = "Baik", this.weatherStatus = "Baik",
this.willRain = false, this.willRain = false,
@ -43,6 +54,8 @@ EvaporasiState copyWith({
EvaporasiViewMode? viewMode, EvaporasiViewMode? viewMode,
List<double>? dailyValues, List<double>? dailyValues,
List<double>? dailyTemperatures, List<double>? dailyTemperatures,
List<String>? chartLabels,
List<Evaporasi>? listData,
List<Evaporasi>? history, List<Evaporasi>? history,
String? weatherStatus, String? weatherStatus,
bool? willRain, bool? willRain,
@ -58,6 +71,8 @@ EvaporasiState copyWith({
dailyValues: dailyValues ?? this.dailyValues, dailyValues: dailyValues ?? this.dailyValues,
dailyTemperatures: dailyTemperatures ?? this.dailyTemperatures, dailyTemperatures: dailyTemperatures ?? this.dailyTemperatures,
history: history ?? this.history, history: history ?? this.history,
chartLabels: chartLabels ?? this.chartLabels,
listData: listData ?? this.listData,
weatherStatus: weatherStatus ?? this.weatherStatus, weatherStatus: weatherStatus ?? this.weatherStatus,
willRain: willRain ?? this.willRain, willRain: willRain ?? this.willRain,
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,

View File

@ -53,7 +53,8 @@ class EvaporasiScreen extends StatelessWidget {
style: style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 15), const SizedBox(height: 15),
// Period selector with date picker
// Period selector with date picker
Builder( Builder(
builder: (context) { builder: (context) {
final state = context.watch<EvaporasiBloc>().state; final state = context.watch<EvaporasiBloc>().state;
@ -83,16 +84,24 @@ class EvaporasiScreen extends StatelessWidget {
Builder( Builder(
builder: (context) { builder: (context) {
final state = context.watch<EvaporasiBloc>().state; final state = context.watch<EvaporasiBloc>().state;
return EvaporasiChartWidget( return Column(
children: [
EvaporasiChartWidget(
dailyValues: state.dailyValues, dailyValues: state.dailyValues,
dailyTemperatures: state.dailyTemperatures, dailyTemperatures: state.dailyTemperatures,
period: state.viewMode == EvaporasiViewMode.customDate period: state.viewMode == EvaporasiViewMode.customDate
? "Tanggal Khusus" ? "Tanggal Khusus"
: state.selectedPeriod, : state.selectedPeriod,
chartLabels: state.chartLabels,
),
const SizedBox(height: 20),
_evaporasiList(state),
const SizedBox(height: 10),
],
); );
}, },
), ),
const SizedBox(height: 25), const SizedBox(height: 10),
ExportPdfButton( ExportPdfButton(
onExport: () => PdfExportService.evaporasi( onExport: () => PdfExportService.evaporasi(
evaporasi: state.currentValue, 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) /// 📅 FORMAT DATE INFO (for custom date display)
/// ========================= /// =========================

View File

@ -5,14 +5,18 @@ class EvaporasiChartWidget extends StatelessWidget {
final List<double> dailyValues; final List<double> dailyValues;
final List<double> dailyTemperatures; final List<double> dailyTemperatures;
final String period; final String period;
final List<String> chartLabels;
const EvaporasiChartWidget({ const EvaporasiChartWidget({
super.key, super.key,
required this.dailyValues, required this.dailyValues,
required this.dailyTemperatures, required this.dailyTemperatures,
required this.period, required this.period,
required this.chartLabels,
}); });
double _getMaxY(List<double> data) { double _getMaxY(List<double> data) {
if (data.isEmpty) return 10; if (data.isEmpty) return 10;
final max = data.reduce((a, b) => a > b ? a : b); final max = data.reduce((a, b) => a > b ? a : b);
@ -93,6 +97,7 @@ class EvaporasiChartWidget extends StatelessWidget {
_getBottomTitle(value), _getBottomTitle(value),
style: const TextStyle( style: const TextStyle(
color: Colors.grey, fontSize: 10), color: Colors.grey, fontSize: 10),
), ),
); );
}, },
@ -227,19 +232,13 @@ class EvaporasiChartWidget extends StatelessWidget {
} }
String _getBottomTitle(double value) { String _getBottomTitle(double value) {
int index = value.toInt(); final index = value.toInt();
final len = dailyValues.length; if (chartLabels.isEmpty) return '';
if (index < 0 || index >= len) return ''; if (index < 0 || index >= chartLabels.length) return '';
return chartLabels[index];
}
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}" : "";
}
}
double _getInterval() { double _getInterval() {
if (period == "Hari Ini") return 1; if (period == "Hari Ini") return 1;

View File

@ -68,7 +68,20 @@ Widget _buildDatePickerButton(BuildContext context, EvaporasiViewMode viewMode,
context.read<EvaporasiBloc>().add( context.read<EvaporasiBloc>().add(
const EvaporasiViewModeChanged(EvaporasiViewMode.customDate), const EvaporasiViewModeChanged(EvaporasiViewMode.customDate),
); );
showEvaporasiDatePicker(context);
// Penting: pastikan bottom sheet masih punya context yang memiliki EvaporasiBloc
final bloc = context.read<EvaporasiBloc>();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (sheetContext) {
return BlocProvider.value(
value: bloc,
child: const EvaporasiDatePicker(),
);
},
);
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),