percobaan
This commit is contained in:
parent
fff7f8b5db
commit
f9abec81c3
|
|
@ -27,29 +27,24 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
on<EvaporasiViewModeChanged>(_onViewModeChanged);
|
on<EvaporasiViewModeChanged>(_onViewModeChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// =========================
|
|
||||||
/// 🚀 START
|
|
||||||
/// =========================
|
|
||||||
Future<void> _onStarted(
|
Future<void> _onStarted(
|
||||||
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));
|
||||||
|
|
||||||
|
// Ambil history yang akan dipakai untuk list + agregasi grafik.
|
||||||
final history = await _repository.getSensorHistory(
|
final history = await _repository.getSensorHistory(
|
||||||
'evaporasi/history',
|
'Monitoring/History',
|
||||||
(json) => Evaporasi.fromJson(json),
|
(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<Evaporasi>.from(history)
|
final listData = List<Evaporasi>.from(history)
|
||||||
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
..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,
|
||||||
|
|
@ -61,7 +56,30 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
getValue: (e) => e.suhu,
|
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 lastValue = history.isNotEmpty ? history.last.evaporasi : 0.0;
|
||||||
final (status, rain) = _computeWeatherStatus(lastValue);
|
final (status, rain) = _computeWeatherStatus(lastValue);
|
||||||
_emitEvaporasiAlert(status, rain, lastValue);
|
_emitEvaporasiAlert(status, rain, lastValue);
|
||||||
|
|
@ -71,9 +89,11 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
listData: listData,
|
listData: listData,
|
||||||
dailyValues: dailyGraph,
|
dailyValues: dailyGraph,
|
||||||
dailyTemperatures: dailyTempGraph,
|
dailyTemperatures: dailyTempGraph,
|
||||||
chartLabels: _buildChartLabels(
|
weeklyValues: weeklyGraph,
|
||||||
period: 'Hari Ini',
|
monthlyValues: monthlyGraph,
|
||||||
),
|
weeklyTemperatures: weeklyTemp,
|
||||||
|
monthlyTemperatures: monthlyTemp,
|
||||||
|
chartLabels: _buildChartLabels(period: 'Hari Ini'),
|
||||||
weatherStatus: status,
|
weatherStatus: status,
|
||||||
willRain: rain,
|
willRain: rain,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|
@ -85,18 +105,16 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
.listen((data) => add(_EvaporasiRealtimeUpdated(data)));
|
.listen((data) => add(_EvaporasiRealtimeUpdated(data)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// =========================
|
|
||||||
/// ⚡ REALTIME
|
|
||||||
/// =========================
|
|
||||||
void _onRealtimeUpdated(
|
void _onRealtimeUpdated(
|
||||||
_EvaporasiRealtimeUpdated event,
|
_EvaporasiRealtimeUpdated event,
|
||||||
Emitter<EvaporasiState> emit,
|
Emitter<EvaporasiState> emit,
|
||||||
) {
|
) {
|
||||||
|
// Hanya update grafik daily (index jam saat ini).
|
||||||
final updated = List<double>.from(state.dailyValues);
|
final updated = List<double>.from(state.dailyValues);
|
||||||
final updatedTemp = List<double>.from(state.dailyTemperatures);
|
final updatedTemp = List<double>.from(state.dailyTemperatures);
|
||||||
final index = DateTime.now().hour;
|
final index = DateTime.now().hour;
|
||||||
|
|
||||||
if (index < updated.length) {
|
if (index >= 0 && index < updated.length) {
|
||||||
updated[index] = event.data.evaporasi;
|
updated[index] = event.data.evaporasi;
|
||||||
updatedTemp[index] = event.data.suhu;
|
updatedTemp[index] = event.data.suhu;
|
||||||
}
|
}
|
||||||
|
|
@ -115,20 +133,18 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// =========================
|
|
||||||
/// 📊 PERIOD
|
|
||||||
/// =========================
|
|
||||||
Future<void> _onPeriodChanged(
|
Future<void> _onPeriodChanged(
|
||||||
EvaporasiPeriodChanged event,
|
EvaporasiPeriodChanged event,
|
||||||
Emitter<EvaporasiState> emit,
|
Emitter<EvaporasiState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(isLoading: true, selectedPeriod: event.period));
|
emit(state.copyWith(isLoading: true, selectedPeriod: event.period));
|
||||||
|
|
||||||
final history = state.history;
|
final history = state.history;
|
||||||
|
|
||||||
List<double> updated;
|
List<double> updated;
|
||||||
List<double> updatedTemp;
|
List<double> updatedTemp;
|
||||||
|
|
||||||
if (event.period == "Minggu Ini") {
|
if (event.period == 'Minggu Ini') {
|
||||||
updated = TimeSeriesMapper.toWeekly(
|
updated = TimeSeriesMapper.toWeekly(
|
||||||
data: history,
|
data: history,
|
||||||
getTime: (e) => e.timestamp,
|
getTime: (e) => e.timestamp,
|
||||||
|
|
@ -139,7 +155,7 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
getTime: (e) => e.timestamp,
|
getTime: (e) => e.timestamp,
|
||||||
getValue: (e) => e.suhu,
|
getValue: (e) => e.suhu,
|
||||||
);
|
);
|
||||||
} else if (event.period == "Bulan Ini") {
|
} else if (event.period == 'Bulan Ini') {
|
||||||
updated = TimeSeriesMapper.toMonthly(
|
updated = TimeSeriesMapper.toMonthly(
|
||||||
data: history,
|
data: history,
|
||||||
getTime: (e) => e.timestamp,
|
getTime: (e) => e.timestamp,
|
||||||
|
|
@ -163,18 +179,17 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pertahankan status cuaca saat ini (tidak berubah karena hanya ganti periode)
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
dailyValues: updated,
|
dailyValues: updated,
|
||||||
dailyTemperatures: updatedTemp,
|
dailyTemperatures: updatedTemp,
|
||||||
chartLabels: _buildChartLabels(period: event.period),
|
chartLabels: _buildChartLabels(period: event.period),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
// penting: jangan hilangkan list
|
||||||
|
listData: state.listData,
|
||||||
|
history: state.history,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// =========================
|
|
||||||
/// 📅 DATE SELECTED (Custom Date)
|
|
||||||
/// =========================
|
|
||||||
Future<void> _onDateSelected(
|
Future<void> _onDateSelected(
|
||||||
EvaporasiDateSelected event,
|
EvaporasiDateSelected event,
|
||||||
Emitter<EvaporasiState> emit,
|
Emitter<EvaporasiState> emit,
|
||||||
|
|
@ -206,18 +221,17 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
dailyTemperatures: updatedTemp,
|
dailyTemperatures: updatedTemp,
|
||||||
chartLabels: _buildChartLabels(period: 'Tanggal Khusus'),
|
chartLabels: _buildChartLabels(period: 'Tanggal Khusus'),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
// jangan hilangkan list
|
||||||
|
listData: state.listData,
|
||||||
|
history: state.history,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// =========================
|
|
||||||
/// 🔄 VIEW MODE CHANGED
|
|
||||||
/// =========================
|
|
||||||
Future<void> _onViewModeChanged(
|
Future<void> _onViewModeChanged(
|
||||||
EvaporasiViewModeChanged event,
|
EvaporasiViewModeChanged event,
|
||||||
Emitter<EvaporasiState> emit,
|
Emitter<EvaporasiState> emit,
|
||||||
) async {
|
) async {
|
||||||
if (event.mode == EvaporasiViewMode.period) {
|
if (event.mode == EvaporasiViewMode.period) {
|
||||||
// Kembali ke mode period - trigger period change
|
|
||||||
add(EvaporasiPeriodChanged(state.selectedPeriod));
|
add(EvaporasiPeriodChanged(state.selectedPeriod));
|
||||||
} else {
|
} else {
|
||||||
emit(state.copyWith(viewMode: event.mode));
|
emit(state.copyWith(viewMode: event.mode));
|
||||||
|
|
@ -230,31 +244,12 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// =========================
|
|
||||||
/// 🌤️ HELPER: Status Cuaca
|
|
||||||
/// =========================
|
|
||||||
static (String status, bool willRain) _computeWeatherStatus(double value) {
|
static (String status, bool willRain) _computeWeatherStatus(double value) {
|
||||||
if (value <= 5.0) return ('Baik', false);
|
if (value <= 5.0) return ('Baik', false);
|
||||||
if (value <= 10.0) return ('Sedang', true);
|
if (value <= 10.0) return ('Sedang', true);
|
||||||
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;
|
||||||
|
|
@ -268,7 +263,7 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
message =
|
message =
|
||||||
'Evaporasi ${value.toStringAsFixed(1)} mm — status sedang, potensi hujan';
|
'Evaporasi ${value.toStringAsFixed(1)} mm — status sedang, potensi hujan';
|
||||||
} else {
|
} else {
|
||||||
severity = AlertSeverity.info; // normal → bersihkan alert
|
severity = AlertSeverity.info;
|
||||||
message = '';
|
message = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -282,9 +277,23 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> _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 {
|
class _EvaporasiRealtimeUpdated extends EvaporasiEvent {
|
||||||
final Evaporasi data;
|
final Evaporasi data;
|
||||||
|
|
||||||
|
|
@ -293,3 +302,4 @@ class _EvaporasiRealtimeUpdated extends EvaporasiEvent {
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [data];
|
List<Object> get props => [data];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,13 @@ class EvaporasiState extends Equatable {
|
||||||
final DateTime? selectedDate; // tanggal spesifik untuk custom date
|
final DateTime? selectedDate; // tanggal spesifik untuk custom date
|
||||||
final EvaporasiViewMode viewMode; // period vs custom date
|
final EvaporasiViewMode viewMode; // period vs custom date
|
||||||
|
|
||||||
final List<double> dailyValues; // untuk grafik evaporasi
|
final List<double> dailyValues; // untuk grafik evaporasi (default)
|
||||||
|
final List<double> weeklyValues;
|
||||||
|
final List<double> monthlyValues;
|
||||||
|
|
||||||
final List<double> dailyTemperatures; // untuk grafik suhu
|
final List<double> dailyTemperatures; // untuk grafik suhu
|
||||||
|
final List<double> weeklyTemperatures;
|
||||||
|
final List<double> monthlyTemperatures;
|
||||||
|
|
||||||
/// Label X-axis sesuai agregasi period
|
/// Label X-axis sesuai agregasi period
|
||||||
/// Contoh: Hari Ini => ["00:00","01:00",...]
|
/// Contoh: Hari Ini => ["00:00","01:00",...]
|
||||||
|
|
@ -21,6 +26,7 @@ class EvaporasiState extends Equatable {
|
||||||
|
|
||||||
/// Data untuk list (timestamp asli dari firebase)
|
/// Data untuk list (timestamp asli dari firebase)
|
||||||
final List<Evaporasi> listData;
|
final List<Evaporasi> listData;
|
||||||
|
|
||||||
final List<Evaporasi> history;
|
final List<Evaporasi> history;
|
||||||
|
|
||||||
final String weatherStatus; // Baik / Sedang / Buruk
|
final String weatherStatus; // Baik / Sedang / Buruk
|
||||||
|
|
@ -32,15 +38,19 @@ class EvaporasiState extends Equatable {
|
||||||
this.currentValue = 0.0,
|
this.currentValue = 0.0,
|
||||||
this.temperature = 0.0,
|
this.temperature = 0.0,
|
||||||
this.waterLevel = 0.0,
|
this.waterLevel = 0.0,
|
||||||
this.selectedPeriod = "Hari Ini",
|
this.selectedPeriod = 'Hari Ini',
|
||||||
this.selectedDate,
|
this.selectedDate,
|
||||||
this.viewMode = EvaporasiViewMode.period,
|
this.viewMode = EvaporasiViewMode.period,
|
||||||
this.dailyValues = const [],
|
this.dailyValues = const [],
|
||||||
|
this.weeklyValues = const [],
|
||||||
|
this.monthlyValues = const [],
|
||||||
this.dailyTemperatures = const [],
|
this.dailyTemperatures = const [],
|
||||||
|
this.weeklyTemperatures = const [],
|
||||||
|
this.monthlyTemperatures = const [],
|
||||||
this.chartLabels = const [],
|
this.chartLabels = const [],
|
||||||
this.listData = const [],
|
this.listData = const [],
|
||||||
this.history = const [],
|
this.history = const [],
|
||||||
this.weatherStatus = "Baik",
|
this.weatherStatus = 'Baik',
|
||||||
this.willRain = false,
|
this.willRain = false,
|
||||||
this.isLoading = true,
|
this.isLoading = true,
|
||||||
});
|
});
|
||||||
|
|
@ -53,7 +63,11 @@ EvaporasiState copyWith({
|
||||||
DateTime? selectedDate,
|
DateTime? selectedDate,
|
||||||
EvaporasiViewMode? viewMode,
|
EvaporasiViewMode? viewMode,
|
||||||
List<double>? dailyValues,
|
List<double>? dailyValues,
|
||||||
|
List<double>? weeklyValues,
|
||||||
|
List<double>? monthlyValues,
|
||||||
List<double>? dailyTemperatures,
|
List<double>? dailyTemperatures,
|
||||||
|
List<double>? weeklyTemperatures,
|
||||||
|
List<double>? monthlyTemperatures,
|
||||||
List<String>? chartLabels,
|
List<String>? chartLabels,
|
||||||
List<Evaporasi>? listData,
|
List<Evaporasi>? listData,
|
||||||
List<Evaporasi>? history,
|
List<Evaporasi>? history,
|
||||||
|
|
@ -69,10 +83,14 @@ EvaporasiState copyWith({
|
||||||
selectedDate: selectedDate ?? this.selectedDate,
|
selectedDate: selectedDate ?? this.selectedDate,
|
||||||
viewMode: viewMode ?? this.viewMode,
|
viewMode: viewMode ?? this.viewMode,
|
||||||
dailyValues: dailyValues ?? this.dailyValues,
|
dailyValues: dailyValues ?? this.dailyValues,
|
||||||
|
weeklyValues: weeklyValues ?? this.weeklyValues,
|
||||||
|
monthlyValues: monthlyValues ?? this.monthlyValues,
|
||||||
dailyTemperatures: dailyTemperatures ?? this.dailyTemperatures,
|
dailyTemperatures: dailyTemperatures ?? this.dailyTemperatures,
|
||||||
history: history ?? this.history,
|
weeklyTemperatures: weeklyTemperatures ?? this.weeklyTemperatures,
|
||||||
|
monthlyTemperatures: monthlyTemperatures ?? this.monthlyTemperatures,
|
||||||
chartLabels: chartLabels ?? this.chartLabels,
|
chartLabels: chartLabels ?? this.chartLabels,
|
||||||
listData: listData ?? this.listData,
|
listData: listData ?? this.listData,
|
||||||
|
history: history ?? this.history,
|
||||||
weatherStatus: weatherStatus ?? this.weatherStatus,
|
weatherStatus: weatherStatus ?? this.weatherStatus,
|
||||||
willRain: willRain ?? this.willRain,
|
willRain: willRain ?? this.willRain,
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
|
@ -88,10 +106,17 @@ EvaporasiState copyWith({
|
||||||
selectedDate,
|
selectedDate,
|
||||||
viewMode,
|
viewMode,
|
||||||
dailyValues,
|
dailyValues,
|
||||||
|
weeklyValues,
|
||||||
|
monthlyValues,
|
||||||
dailyTemperatures,
|
dailyTemperatures,
|
||||||
|
weeklyTemperatures,
|
||||||
|
monthlyTemperatures,
|
||||||
|
chartLabels,
|
||||||
|
listData,
|
||||||
history,
|
history,
|
||||||
weatherStatus,
|
weatherStatus,
|
||||||
willRain,
|
willRain,
|
||||||
isLoading,
|
isLoading,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,21 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import '../blocs/evaporasi_bloc.dart';
|
import '../blocs/evaporasi_bloc.dart';
|
||||||
import 'widgets/evaporasi_chart_widget.dart';
|
import 'widgets/evaporasi_chart_widget.dart';
|
||||||
import 'widgets/evaporasi_period_selector.dart';
|
import 'widgets/evaporasi_period_selector.dart';
|
||||||
import '../../shared/utils/pdf/pdf_export_service.dart';
|
import '../../shared/utils/pdf/pdf_export_service.dart';
|
||||||
import '../../shared/widgets/export_pdf_button.dart';
|
import '../../shared/widgets/export_pdf_button.dart';
|
||||||
|
|
||||||
class EvaporasiScreen extends StatelessWidget {
|
class EvaporasiScreen extends StatefulWidget {
|
||||||
const EvaporasiScreen({super.key});
|
const EvaporasiScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EvaporasiScreen> createState() => _EvaporasiScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EvaporasiScreenState extends State<EvaporasiScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -49,9 +55,10 @@ class EvaporasiScreen extends StatelessWidget {
|
||||||
const SizedBox(height: 25),
|
const SizedBox(height: 25),
|
||||||
_statusCard(state),
|
_statusCard(state),
|
||||||
const SizedBox(height: 25),
|
const SizedBox(height: 25),
|
||||||
const Text("Tren Evaporasi & Suhu",
|
const Text(
|
||||||
style:
|
"Tren Evaporasi & Suhu",
|
||||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
// Period selector with date picker
|
// Period selector with date picker
|
||||||
|
|
@ -61,7 +68,6 @@ class EvaporasiScreen extends StatelessWidget {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Show selected date info when in custom mode
|
|
||||||
if (state.viewMode == EvaporasiViewMode.customDate &&
|
if (state.viewMode == EvaporasiViewMode.customDate &&
|
||||||
state.selectedDate != null)
|
state.selectedDate != null)
|
||||||
Padding(
|
Padding(
|
||||||
|
|
@ -80,28 +86,32 @@ class EvaporasiScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// Chart
|
||||||
Builder(
|
Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final state = context.watch<EvaporasiBloc>().state;
|
final state = context.watch<EvaporasiBloc>().state;
|
||||||
return Column(
|
return EvaporasiChartWidget(
|
||||||
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,
|
chartLabels: state.chartLabels,
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
_evaporasiList(state),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// List data (mengikuti chart: period vs custom date)
|
||||||
|
_evaporasiList(state),
|
||||||
|
|
||||||
|
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
ExportPdfButton(
|
ExportPdfButton(
|
||||||
onExport: () => PdfExportService.evaporasi(
|
onExport: () => PdfExportService.evaporasi(
|
||||||
evaporasi: state.currentValue,
|
evaporasi: state.currentValue,
|
||||||
|
|
@ -224,6 +234,20 @@ class EvaporasiScreen extends StatelessWidget {
|
||||||
break;
|
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(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
|
|
@ -246,22 +270,28 @@ class EvaporasiScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
state.weatherStatus,
|
state.weatherStatus == 'Baik'
|
||||||
|
? 'Normal'
|
||||||
|
: state.weatherStatus == 'Sedang'
|
||||||
|
? 'Sedang'
|
||||||
|
: 'Tinggi',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: statusColor,
|
color: statusColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
if (state.willRain)
|
if (state.willRain)
|
||||||
const Text(
|
Text(
|
||||||
"⚠️ Potensi hujan tinggi",
|
warningText,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -274,7 +304,15 @@ class EvaporasiScreen extends StatelessWidget {
|
||||||
/// 🧾 LIST DATA EVAPORASI
|
/// 🧾 LIST DATA EVAPORASI
|
||||||
/// =========================
|
/// =========================
|
||||||
Widget _evaporasiList(EvaporasiState state) {
|
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) {
|
if (data.isEmpty) {
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
|
@ -283,8 +321,10 @@ class EvaporasiScreen extends StatelessWidget {
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Text('Belum ada data evaporasi',
|
child: const Text(
|
||||||
style: TextStyle(color: Colors.grey)),
|
'Belum ada data evaporasi',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,17 +343,14 @@ class EvaporasiScreen extends StatelessWidget {
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
ListView.separated(
|
SizedBox(
|
||||||
shrinkWrap: true,
|
height: 280,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
child: ListView.separated(
|
||||||
itemCount: data.length,
|
itemCount: data.length,
|
||||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final e = data[index];
|
final e = data[index];
|
||||||
final dateLabel = DateFormat('dd MMM yyyy', 'id_ID')
|
final dateLabel =
|
||||||
.format(e.timestamp) +
|
'${DateFormat('dd MMM yyyy', 'id_ID').format(e.timestamp)} • ${DateFormat('HH:mm:ss', 'id_ID').format(e.timestamp)}';
|
||||||
' • ' +
|
|
||||||
DateFormat('HH:mm:ss', 'id_ID').format(e.timestamp);
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
|
@ -344,19 +381,39 @@ class EvaporasiScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'${e.suhu.toStringAsFixed(1)} °C',
|
'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(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.orange.shade700,
|
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)
|
/// 📅 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) {
|
String _formatDateInfo(DateTime date) {
|
||||||
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
final yesterday = today.subtract(const Duration(days: 1));
|
final yesterday = today.subtract(const Duration(days: 1));
|
||||||
final selected = DateTime(date.year, date.month, date.day);
|
final selected = DateTime(date.year, date.month, date.day);
|
||||||
|
|
@ -381,3 +452,4 @@ class EvaporasiScreen extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,18 +72,42 @@ class EvaporasiChartWidget extends StatelessWidget {
|
||||||
duration: const Duration(milliseconds: 600),
|
duration: const Duration(milliseconds: 600),
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
LineChartData(
|
LineChartData(
|
||||||
|
|
||||||
minY: 0,
|
minY: 0,
|
||||||
maxY: maxY,
|
maxY: maxY,
|
||||||
gridData: const FlGridData(show: false),
|
gridData: const FlGridData(show: false),
|
||||||
borderData: FlBorderData(show: false),
|
borderData: FlBorderData(show: false),
|
||||||
titlesData: FlTitlesData(
|
titlesData: FlTitlesData(
|
||||||
show: true,
|
show: true,
|
||||||
rightTitles: const AxisTitles(
|
// dual axis: kiri untuk Evaporasi (mm), kanan untuk Suhu (°C)
|
||||||
sideTitles: SideTitles(showTitles: false)),
|
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(
|
topTitles: const AxisTitles(
|
||||||
sideTitles: SideTitles(showTitles: false)),
|
sideTitles: SideTitles(showTitles: false)),
|
||||||
leftTitles: const AxisTitles(
|
|
||||||
sideTitles: SideTitles(showTitles: false)),
|
|
||||||
bottomTitles: AxisTitles(
|
bottomTitles: AxisTitles(
|
||||||
sideTitles: SideTitles(
|
sideTitles: SideTitles(
|
||||||
showTitles: true,
|
showTitles: true,
|
||||||
|
|
@ -105,7 +129,7 @@ class EvaporasiChartWidget extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
lineBarsData: [
|
lineBarsData: [
|
||||||
// === Garis Evaporasi ===
|
// === Garis Evaporasi (kiri axis) ===
|
||||||
LineChartBarData(
|
LineChartBarData(
|
||||||
spots: safeValues.asMap().entries.map((e) {
|
spots: safeValues.asMap().entries.map((e) {
|
||||||
return FlSpot(e.key.toDouble(), e.value);
|
return FlSpot(e.key.toDouble(), e.value);
|
||||||
|
|
@ -143,7 +167,7 @@ class EvaporasiChartWidget extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// === Garis Suhu ===
|
// === Garis Suhu (kanan axis) ===
|
||||||
LineChartBarData(
|
LineChartBarData(
|
||||||
spots: safeTemps.asMap().entries.map((e) {
|
spots: safeTemps.asMap().entries.map((e) {
|
||||||
return FlSpot(e.key.toDouble(), e.value);
|
return FlSpot(e.key.toDouble(), e.value);
|
||||||
|
|
@ -245,5 +269,15 @@ class EvaporasiChartWidget extends StatelessWidget {
|
||||||
if (period == "Minggu Ini") return 1;
|
if (period == "Minggu Ini") return 1;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double _getYAxisInterval(List<double> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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<String> onQueryChanged;
|
||||||
|
|
||||||
|
const EvaporasiDateSearchBar({
|
||||||
|
super.key,
|
||||||
|
required this.initialQuery,
|
||||||
|
required this.onQueryChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EvaporasiDateSearchBar> createState() => _EvaporasiDateSearchBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EvaporasiDateSearchBarState extends State<EvaporasiDateSearchBar> {
|
||||||
|
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(() {});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -32,9 +32,12 @@ class Evaporasi implements HasTimestamp {
|
||||||
final suhu = (json['suhu'] ?? json['suhu_air'] ?? 0).toDouble();
|
final suhu = (json['suhu'] ?? json['suhu_air'] ?? 0).toDouble();
|
||||||
final tinggi = (json['tinggi'] ?? json['tinggi_air'] ?? 0).toDouble();
|
final tinggi = (json['tinggi'] ?? json['tinggi_air'] ?? 0).toDouble();
|
||||||
|
|
||||||
// ✅ Prioritas: timestamp (Unix, kalau firmware sudah diupdate)
|
// ✅ Prioritas waktu sesuai firmware ESP32:
|
||||||
// → fallback ke waktu ("HH:MM:SS")
|
// 1) timestamp (Unix, kalau pernah dikirim)
|
||||||
final rawTime = json['timestamp'] ?? json['waktu'];
|
// 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(
|
return Evaporasi(
|
||||||
evaporasi: (json['evaporasi'] ?? 0).toDouble(),
|
evaporasi: (json['evaporasi'] ?? 0).toDouble(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue