Fix evaporasi dashboard: firebase history list + sync chart labels
This commit is contained in:
parent
6ef9ab0985
commit
42bac0c980
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
dailyValues: state.dailyValues,
|
children: [
|
||||||
dailyTemperatures: state.dailyTemperatures,
|
EvaporasiChartWidget(
|
||||||
period: state.viewMode == EvaporasiViewMode.customDate
|
dailyValues: state.dailyValues,
|
||||||
? "Tanggal Khusus"
|
dailyTemperatures: state.dailyTemperatures,
|
||||||
: state.selectedPeriod,
|
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(
|
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)
|
||||||
/// =========================
|
/// =========================
|
||||||
|
|
|
||||||
|
|
@ -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,20 +232,14 @@ 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;
|
||||||
if (period == "Minggu Ini") return 1;
|
if (period == "Minggu Ini") return 1;
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue