coba
This commit is contained in:
parent
f976556d99
commit
4fd56dc5f9
|
|
@ -10,10 +10,8 @@ import '../../../../blocs/notification_bloc/notification_bloc.dart';
|
||||||
part 'wind_speed_event.dart';
|
part 'wind_speed_event.dart';
|
||||||
part 'wind_speed_state.dart';
|
part 'wind_speed_state.dart';
|
||||||
|
|
||||||
/// Threshold kecepatan angin (satuan m/s)
|
const double _kWindWarning = 25.0;
|
||||||
/// Referensi: Beaufort scale & BMKG
|
const double _kWindDanger = 45.5;
|
||||||
const double _kWindWarning = 25.0; // Waspada: 8–12.5 m/s (~29–45 km/h)
|
|
||||||
const double _kWindDanger = 45.5; // Bahaya: > 12.5 m/s (> 45 km/h)
|
|
||||||
const _kDeviceIdKey = 'selected_device_id';
|
const _kDeviceIdKey = 'selected_device_id';
|
||||||
const _kDefaultDeviceId = 'esp_lapangan';
|
const _kDefaultDeviceId = 'esp_lapangan';
|
||||||
|
|
||||||
|
|
@ -31,70 +29,51 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
on<WatchWindSpeedStarted>(_onStarted, transformer: restartable());
|
on<WatchWindSpeedStarted>(_onStarted, transformer: restartable());
|
||||||
on<_WindSpeedRealtimeUpdated>(_onRealtimeUpdated);
|
on<_WindSpeedRealtimeUpdated>(_onRealtimeUpdated);
|
||||||
on<WindSpeedPeriodChanged>(_onPeriodChanged);
|
on<WindSpeedPeriodChanged>(_onPeriodChanged);
|
||||||
on<WindSpeedDateFilterChanged>(_onDateFilterChanged); // ← baru
|
on<WindSpeedDateFilterChanged>(_onDateFilterChanged);
|
||||||
|
// Delete handlers
|
||||||
|
on<WindSpeedDeleteAllRequested>(_onDeleteAll);
|
||||||
|
on<WindSpeedDeleteByDateRequested>(_onDeleteByDate);
|
||||||
|
on<WindSpeedDeleteByDateRangeRequested>(_onDeleteByDateRange);
|
||||||
|
on<WindSpeedDeleteByHourRangeRequested>(_onDeleteByHourRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Baca device ID dari SharedPreferences ─────────────────
|
|
||||||
Future<String> _getDeviceId() async {
|
Future<String> _getDeviceId() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
return prefs.getString(_kDeviceIdKey) ?? _kDefaultDeviceId;
|
return prefs.getString(_kDeviceIdKey) ?? _kDefaultDeviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
// START
|
// START — sekarang fetch dengan keys untuk delete support
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
Future<void> _onStarted(
|
Future<void> _onStarted(
|
||||||
WatchWindSpeedStarted event,
|
WatchWindSpeedStarted event,
|
||||||
Emitter<WindSpeedState> emit,
|
Emitter<WindSpeedState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(isLoading: true));
|
emit(state.copyWith(isLoading: true));
|
||||||
|
|
||||||
// Baca device ID aktif (dari SharedPreferences, diset di DeviceSetupBloc)
|
|
||||||
final deviceId = await _getDeviceId();
|
final deviceId = await _getDeviceId();
|
||||||
|
|
||||||
final history = await _repository.getSensorHistory(
|
final historyMap = await _repository.getSensorHistoryWithKeys(
|
||||||
'anemometer/$deviceId/history',
|
'anemometer/$deviceId/history',
|
||||||
(json) => MyWindSpeed.fromJson(json),
|
(json) => MyWindSpeed.fromJson(json),
|
||||||
);
|
);
|
||||||
|
|
||||||
final dailyGraph = TimeSeriesMapper.smooth(
|
final history = _sortedList(historyMap);
|
||||||
TimeSeriesMapper.toDaily(
|
final graphs = _buildGraphs(history);
|
||||||
data: history,
|
|
||||||
getTime: (e) => e.timestamp,
|
|
||||||
getValue: (e) => e.speed,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final weekly = TimeSeriesMapper.smooth(
|
|
||||||
TimeSeriesMapper.toWeekly(
|
|
||||||
data: history,
|
|
||||||
getTime: (e) => e.timestamp,
|
|
||||||
getValue: (e) => e.speed,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final monthly = TimeSeriesMapper.smooth(
|
|
||||||
TimeSeriesMapper.toMonthly(
|
|
||||||
data: history,
|
|
||||||
getTime: (e) => e.timestamp,
|
|
||||||
getValue: (e) => e.speed,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (history.isNotEmpty) {
|
if (history.isNotEmpty) _emitWindAlert(history.last.speed);
|
||||||
_emitWindAlert(history.last.speed);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
|
historyMap: historyMap,
|
||||||
history: history,
|
history: history,
|
||||||
filteredHistory: history, // awal = semua data
|
filteredHistory: history,
|
||||||
dailySpeeds: dailyGraph,
|
dailySpeeds: graphs['daily']!,
|
||||||
weeklySpeeds: weekly,
|
weeklySpeeds: graphs['weekly']!,
|
||||||
monthlySpeeds: monthly,
|
monthlySpeeds: graphs['monthly']!,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
alertLevel:
|
alertLevel:
|
||||||
history.isNotEmpty ? _getAlertLevel(history.last.speed) : 'Normal',
|
history.isNotEmpty ? _getAlertLevel(history.last.speed) : 'Normal',
|
||||||
));
|
));
|
||||||
|
|
||||||
// Subscribe realtime dari path device aktif
|
|
||||||
await _subscription?.cancel();
|
await _subscription?.cancel();
|
||||||
_subscription = _repository
|
_subscription = _repository
|
||||||
.getSensorStream(
|
.getSensorStream(
|
||||||
|
|
@ -128,7 +107,6 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
_emitWindAlert(newValue);
|
_emitWindAlert(newValue);
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
currentSpeed: newValue,
|
currentSpeed: newValue,
|
||||||
dailySpeeds: updated,
|
dailySpeeds: updated,
|
||||||
|
|
@ -168,7 +146,6 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final updatedGraph = TimeSeriesMapper.smooth(raw);
|
final updatedGraph = TimeSeriesMapper.smooth(raw);
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
dailySpeeds:
|
dailySpeeds:
|
||||||
event.period == 'Hari Ini' ? updatedGraph : state.dailySpeeds,
|
event.period == 'Hari Ini' ? updatedGraph : state.dailySpeeds,
|
||||||
|
|
@ -176,45 +153,236 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
event.period == 'Minggu Ini' ? updatedGraph : state.weeklySpeeds,
|
event.period == 'Minggu Ini' ? updatedGraph : state.weeklySpeeds,
|
||||||
monthlySpeeds:
|
monthlySpeeds:
|
||||||
event.period == 'Bulan Ini' ? updatedGraph : state.monthlySpeeds,
|
event.period == 'Bulan Ini' ? updatedGraph : state.monthlySpeeds,
|
||||||
isLoading: false,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
// DATE FILTER CHANGED ← baru
|
// DATE FILTER
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
void _onDateFilterChanged(
|
void _onDateFilterChanged(
|
||||||
WindSpeedDateFilterChanged event,
|
WindSpeedDateFilterChanged event,
|
||||||
Emitter<WindSpeedState> emit,
|
Emitter<WindSpeedState> emit,
|
||||||
) {
|
) {
|
||||||
final date = event.date;
|
final date = event.date;
|
||||||
final allHistory = state.history;
|
|
||||||
|
|
||||||
if (date == null) {
|
if (date == null) {
|
||||||
// Reset: tampilkan semua
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
filteredHistory: allHistory,
|
filteredHistory: state.history,
|
||||||
clearSelectedDate: true,
|
clearSelectedDate: true,
|
||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter data yang tanggalnya sama dengan [date]
|
final filtered =
|
||||||
final filtered = allHistory.where((item) {
|
state.history.where((e) => _isSameDate(e.timestamp, date)).toList();
|
||||||
return item.timestamp.year == date.year &&
|
|
||||||
item.timestamp.month == date.month &&
|
|
||||||
item.timestamp.day == date.day;
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(filteredHistory: filtered, selectedDate: date));
|
||||||
filteredHistory: filtered,
|
}
|
||||||
selectedDate: date,
|
|
||||||
));
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// DELETE ALL
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
Future<void> _onDeleteAll(
|
||||||
|
WindSpeedDeleteAllRequested event,
|
||||||
|
Emitter<WindSpeedState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(isDeleting: true, clearDeleteError: true));
|
||||||
|
try {
|
||||||
|
final deviceId = await _getDeviceId();
|
||||||
|
await _repository.deleteAllHistory('anemometer/$deviceId/history');
|
||||||
|
final emptyGraphs = _buildGraphs(const []);
|
||||||
|
emit(state.copyWith(
|
||||||
|
isDeleting: false,
|
||||||
|
historyMap: const {},
|
||||||
|
history: const [],
|
||||||
|
filteredHistory: const [],
|
||||||
|
clearSelectedDate: true,
|
||||||
|
dailySpeeds: emptyGraphs['daily'],
|
||||||
|
weeklySpeeds: emptyGraphs['weekly'],
|
||||||
|
monthlySpeeds: emptyGraphs['monthly'],
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(isDeleting: false, deleteError: e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// DELETE BY DATE
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
Future<void> _onDeleteByDate(
|
||||||
|
WindSpeedDeleteByDateRequested event,
|
||||||
|
Emitter<WindSpeedState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(isDeleting: true, clearDeleteError: true));
|
||||||
|
try {
|
||||||
|
final deviceId = await _getDeviceId();
|
||||||
|
|
||||||
|
final keys = state.historyMap.entries
|
||||||
|
.where((e) => _isSameDate(e.value.timestamp, event.date))
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (keys.isNotEmpty) {
|
||||||
|
await _repository.deleteHistoryByKeys(
|
||||||
|
'anemometer/$deviceId/history', keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
_emitAfterDelete(emit, _removeKeys(state.historyMap, keys));
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(isDeleting: false, deleteError: e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// DELETE BY DATE RANGE
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
Future<void> _onDeleteByDateRange(
|
||||||
|
WindSpeedDeleteByDateRangeRequested event,
|
||||||
|
Emitter<WindSpeedState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(isDeleting: true, clearDeleteError: true));
|
||||||
|
try {
|
||||||
|
final deviceId = await _getDeviceId();
|
||||||
|
|
||||||
|
final startDay =
|
||||||
|
DateTime(event.start.year, event.start.month, event.start.day);
|
||||||
|
final endInclusive =
|
||||||
|
DateTime(event.end.year, event.end.month, event.end.day, 23, 59, 59);
|
||||||
|
|
||||||
|
final keys = state.historyMap.entries
|
||||||
|
.where((e) {
|
||||||
|
final t = e.value.timestamp;
|
||||||
|
return !t.isBefore(startDay) && !t.isAfter(endInclusive);
|
||||||
|
})
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (keys.isNotEmpty) {
|
||||||
|
await _repository.deleteHistoryByKeys(
|
||||||
|
'anemometer/$deviceId/history', keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
_emitAfterDelete(emit, _removeKeys(state.historyMap, keys));
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(isDeleting: false, deleteError: e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// DELETE BY HOUR RANGE
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
Future<void> _onDeleteByHourRange(
|
||||||
|
WindSpeedDeleteByHourRangeRequested event,
|
||||||
|
Emitter<WindSpeedState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(isDeleting: true, clearDeleteError: true));
|
||||||
|
try {
|
||||||
|
final deviceId = await _getDeviceId();
|
||||||
|
|
||||||
|
final keys = state.historyMap.entries
|
||||||
|
.where((e) {
|
||||||
|
final t = e.value.timestamp;
|
||||||
|
return _isSameDate(t, event.date) &&
|
||||||
|
t.hour >= event.startHour &&
|
||||||
|
t.hour <= event.endHour;
|
||||||
|
})
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (keys.isNotEmpty) {
|
||||||
|
await _repository.deleteHistoryByKeys(
|
||||||
|
'anemometer/$deviceId/history', keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-apply filter aktif jika ada
|
||||||
|
final newMap = _removeKeys(state.historyMap, keys);
|
||||||
|
final newHistory = _sortedList(newMap);
|
||||||
|
final newFiltered = state.selectedDate != null
|
||||||
|
? newHistory
|
||||||
|
.where((e) => _isSameDate(e.timestamp, state.selectedDate!))
|
||||||
|
.toList()
|
||||||
|
: newHistory;
|
||||||
|
final graphs = _buildGraphs(newHistory);
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
isDeleting: false,
|
||||||
|
historyMap: newMap,
|
||||||
|
history: newHistory,
|
||||||
|
filteredHistory: newFiltered,
|
||||||
|
dailySpeeds: graphs['daily'],
|
||||||
|
weeklySpeeds: graphs['weekly'],
|
||||||
|
monthlySpeeds: graphs['monthly'],
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(isDeleting: false, deleteError: e.toString()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
// HELPERS
|
// HELPERS
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Emit state setelah delete all/date/range (selalu reset filter)
|
||||||
|
void _emitAfterDelete(
|
||||||
|
Emitter<WindSpeedState> emit,
|
||||||
|
Map<String, MyWindSpeed> newMap,
|
||||||
|
) {
|
||||||
|
final newHistory = _sortedList(newMap);
|
||||||
|
final graphs = _buildGraphs(newHistory);
|
||||||
|
emit(state.copyWith(
|
||||||
|
isDeleting: false,
|
||||||
|
historyMap: newMap,
|
||||||
|
history: newHistory,
|
||||||
|
filteredHistory: newHistory,
|
||||||
|
clearSelectedDate: true,
|
||||||
|
dailySpeeds: graphs['daily'],
|
||||||
|
weeklySpeeds: graphs['weekly'],
|
||||||
|
monthlySpeeds: graphs['monthly'],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<double>> _buildGraphs(List<MyWindSpeed> history) {
|
||||||
|
return {
|
||||||
|
'daily': TimeSeriesMapper.smooth(
|
||||||
|
TimeSeriesMapper.toDaily(
|
||||||
|
data: history,
|
||||||
|
getTime: (e) => e.timestamp,
|
||||||
|
getValue: (e) => e.speed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'weekly': TimeSeriesMapper.smooth(
|
||||||
|
TimeSeriesMapper.toWeekly(
|
||||||
|
data: history,
|
||||||
|
getTime: (e) => e.timestamp,
|
||||||
|
getValue: (e) => e.speed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'monthly': TimeSeriesMapper.smooth(
|
||||||
|
TimeSeriesMapper.toMonthly(
|
||||||
|
data: history,
|
||||||
|
getTime: (e) => e.timestamp,
|
||||||
|
getValue: (e) => e.speed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isSameDate(DateTime a, DateTime b) =>
|
||||||
|
a.year == b.year && a.month == b.month && a.day == b.day;
|
||||||
|
|
||||||
|
Map<String, MyWindSpeed> _removeKeys(
|
||||||
|
Map<String, MyWindSpeed> map,
|
||||||
|
List<String> keys,
|
||||||
|
) {
|
||||||
|
final newMap = Map<String, MyWindSpeed>.from(map);
|
||||||
|
for (final k in keys) newMap.remove(k);
|
||||||
|
return newMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MyWindSpeed> _sortedList(Map<String, MyWindSpeed> map) {
|
||||||
|
return map.values.toList()
|
||||||
|
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||||
|
}
|
||||||
|
|
||||||
String _getAlertLevel(double speed) {
|
String _getAlertLevel(double speed) {
|
||||||
if (speed >= _kWindDanger) return 'Bahaya';
|
if (speed >= _kWindDanger) return 'Bahaya';
|
||||||
if (speed >= _kWindWarning) return 'Waspada';
|
if (speed >= _kWindWarning) return 'Waspada';
|
||||||
|
|
@ -236,15 +404,13 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
message = '';
|
message = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
_notificationBloc.add(SensorAlertAdded(
|
_notificationBloc.add(SensorAlertAdded(SensorAlert(
|
||||||
SensorAlert(
|
sensorId: 'wind_speed',
|
||||||
sensorId: 'wind_speed',
|
sensorName: 'Anemometer',
|
||||||
sensorName: 'Anemometer',
|
message: message,
|
||||||
message: message,
|
severity: severity,
|
||||||
severity: severity,
|
timestamp: DateTime.now(),
|
||||||
timestamp: DateTime.now(),
|
)));
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -254,7 +420,6 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Internal event — tidak diekspos ke luar
|
|
||||||
class _WindSpeedRealtimeUpdated extends WindSpeedEvent {
|
class _WindSpeedRealtimeUpdated extends WindSpeedEvent {
|
||||||
final MyWindSpeed data;
|
final MyWindSpeed data;
|
||||||
const _WindSpeedRealtimeUpdated(this.data);
|
const _WindSpeedRealtimeUpdated(this.data);
|
||||||
|
|
|
||||||
|
|
@ -30,3 +30,49 @@ class WindSpeedDateFilterChanged extends WindSpeedEvent {
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [date];
|
List<Object?> get props => [date];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// DELETE EVENTS
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Hapus SEMUA riwayat dari Firebase
|
||||||
|
class WindSpeedDeleteAllRequested extends WindSpeedEvent {
|
||||||
|
const WindSpeedDeleteAllRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hapus riwayat pada tanggal tertentu
|
||||||
|
class WindSpeedDeleteByDateRequested extends WindSpeedEvent {
|
||||||
|
final DateTime date;
|
||||||
|
const WindSpeedDeleteByDateRequested(this.date);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [date];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hapus riwayat dalam rentang tanggal (inklusif)
|
||||||
|
class WindSpeedDeleteByDateRangeRequested extends WindSpeedEvent {
|
||||||
|
final DateTime start;
|
||||||
|
final DateTime end;
|
||||||
|
const WindSpeedDeleteByDateRangeRequested({
|
||||||
|
required this.start,
|
||||||
|
required this.end,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [start, end];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hapus riwayat dalam rentang jam pada tanggal tertentu
|
||||||
|
class WindSpeedDeleteByHourRangeRequested extends WindSpeedEvent {
|
||||||
|
final DateTime date;
|
||||||
|
final int startHour; // 0–23
|
||||||
|
final int endHour; // 0–23, harus >= startHour
|
||||||
|
const WindSpeedDeleteByHourRangeRequested({
|
||||||
|
required this.date,
|
||||||
|
required this.startHour,
|
||||||
|
required this.endHour,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [date, startHour, endHour];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ part of 'wind_speed_bloc.dart';
|
||||||
|
|
||||||
class WindSpeedState extends Equatable {
|
class WindSpeedState extends Equatable {
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
|
final bool isDeleting;
|
||||||
|
final String? deleteError;
|
||||||
final double currentSpeed;
|
final double currentSpeed;
|
||||||
final String alertLevel;
|
final String alertLevel;
|
||||||
final String selectedPeriod;
|
final String selectedPeriod;
|
||||||
|
|
@ -16,8 +18,13 @@ class WindSpeedState extends Equatable {
|
||||||
final List<MyWindSpeed> filteredHistory; // setelah difilter tanggal
|
final List<MyWindSpeed> filteredHistory; // setelah difilter tanggal
|
||||||
final DateTime? selectedDate; // null = tampilkan semua
|
final DateTime? selectedDate; // null = tampilkan semua
|
||||||
|
|
||||||
|
/// Map { Firebase push-key → data } — dipakai untuk operasi delete
|
||||||
|
final Map<String, MyWindSpeed> historyMap;
|
||||||
|
|
||||||
const WindSpeedState({
|
const WindSpeedState({
|
||||||
this.isLoading = false,
|
this.isLoading = false,
|
||||||
|
this.isDeleting = false,
|
||||||
|
this.deleteError,
|
||||||
this.currentSpeed = 0.0,
|
this.currentSpeed = 0.0,
|
||||||
this.alertLevel = 'Normal',
|
this.alertLevel = 'Normal',
|
||||||
this.selectedPeriod = 'Hari Ini',
|
this.selectedPeriod = 'Hari Ini',
|
||||||
|
|
@ -27,10 +34,14 @@ class WindSpeedState extends Equatable {
|
||||||
this.history = const [],
|
this.history = const [],
|
||||||
this.filteredHistory = const [],
|
this.filteredHistory = const [],
|
||||||
this.selectedDate,
|
this.selectedDate,
|
||||||
|
this.historyMap = const {},
|
||||||
});
|
});
|
||||||
|
|
||||||
WindSpeedState copyWith({
|
WindSpeedState copyWith({
|
||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
|
bool? isDeleting,
|
||||||
|
String? deleteError,
|
||||||
|
bool clearDeleteError = false,
|
||||||
double? currentSpeed,
|
double? currentSpeed,
|
||||||
String? alertLevel,
|
String? alertLevel,
|
||||||
String? selectedPeriod,
|
String? selectedPeriod,
|
||||||
|
|
@ -41,9 +52,12 @@ class WindSpeedState extends Equatable {
|
||||||
List<MyWindSpeed>? filteredHistory,
|
List<MyWindSpeed>? filteredHistory,
|
||||||
DateTime? selectedDate,
|
DateTime? selectedDate,
|
||||||
bool clearSelectedDate = false,
|
bool clearSelectedDate = false,
|
||||||
|
Map<String, MyWindSpeed>? historyMap,
|
||||||
}) {
|
}) {
|
||||||
return WindSpeedState(
|
return WindSpeedState(
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
isDeleting: isDeleting ?? this.isDeleting,
|
||||||
|
deleteError: clearDeleteError ? null : (deleteError ?? this.deleteError),
|
||||||
currentSpeed: currentSpeed ?? this.currentSpeed,
|
currentSpeed: currentSpeed ?? this.currentSpeed,
|
||||||
alertLevel: alertLevel ?? this.alertLevel,
|
alertLevel: alertLevel ?? this.alertLevel,
|
||||||
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
||||||
|
|
@ -54,12 +68,15 @@ class WindSpeedState extends Equatable {
|
||||||
filteredHistory: filteredHistory ?? this.filteredHistory,
|
filteredHistory: filteredHistory ?? this.filteredHistory,
|
||||||
selectedDate:
|
selectedDate:
|
||||||
clearSelectedDate ? null : (selectedDate ?? this.selectedDate),
|
clearSelectedDate ? null : (selectedDate ?? this.selectedDate),
|
||||||
|
historyMap: historyMap ?? this.historyMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isDeleting,
|
||||||
|
deleteError,
|
||||||
currentSpeed,
|
currentSpeed,
|
||||||
alertLevel,
|
alertLevel,
|
||||||
selectedPeriod,
|
selectedPeriod,
|
||||||
|
|
@ -69,5 +86,6 @@ class WindSpeedState extends Equatable {
|
||||||
history,
|
history,
|
||||||
filteredHistory,
|
filteredHistory,
|
||||||
selectedDate,
|
selectedDate,
|
||||||
|
historyMap,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ class WindSpeedHistoryList extends StatelessWidget {
|
||||||
final DateTime? selectedDate;
|
final DateTime? selectedDate;
|
||||||
final VoidCallback onPickDate;
|
final VoidCallback onPickDate;
|
||||||
final VoidCallback onClearDate;
|
final VoidCallback onClearDate;
|
||||||
|
final VoidCallback onDeleteTap;
|
||||||
|
final bool isDeleting;
|
||||||
|
|
||||||
const WindSpeedHistoryList({
|
const WindSpeedHistoryList({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -19,6 +21,8 @@ class WindSpeedHistoryList extends StatelessWidget {
|
||||||
required this.selectedDate,
|
required this.selectedDate,
|
||||||
required this.onPickDate,
|
required this.onPickDate,
|
||||||
required this.onClearDate,
|
required this.onClearDate,
|
||||||
|
required this.onDeleteTap,
|
||||||
|
this.isDeleting = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Grouping per tanggal ─────────────────────────────────────
|
// ── Grouping per tanggal ─────────────────────────────────────
|
||||||
|
|
@ -46,6 +50,8 @@ class WindSpeedHistoryList extends StatelessWidget {
|
||||||
totalCount: history.length,
|
totalCount: history.length,
|
||||||
onPickDate: onPickDate,
|
onPickDate: onPickDate,
|
||||||
onClearDate: onClearDate,
|
onClearDate: onClearDate,
|
||||||
|
onDeleteTap: onDeleteTap, // ← baru
|
||||||
|
isDeleting: isDeleting,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
|
@ -98,12 +104,16 @@ class _HeaderBar extends StatelessWidget {
|
||||||
final int totalCount;
|
final int totalCount;
|
||||||
final VoidCallback onPickDate;
|
final VoidCallback onPickDate;
|
||||||
final VoidCallback onClearDate;
|
final VoidCallback onClearDate;
|
||||||
|
final VoidCallback onDeleteTap;
|
||||||
|
final bool isDeleting;
|
||||||
|
|
||||||
const _HeaderBar({
|
const _HeaderBar({
|
||||||
required this.selectedDate,
|
required this.selectedDate,
|
||||||
required this.totalCount,
|
required this.totalCount,
|
||||||
required this.onPickDate,
|
required this.onPickDate,
|
||||||
required this.onClearDate,
|
required this.onClearDate,
|
||||||
|
required this.onDeleteTap,
|
||||||
|
required this.isDeleting,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -148,6 +158,25 @@ class _HeaderBar extends StatelessWidget {
|
||||||
color: Colors.blue.shade700,
|
color: Colors.blue.shade700,
|
||||||
onTap: onPickDate,
|
onTap: onPickDate,
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
|
||||||
|
// ── DELETE BUTTON ← baru ──────────────────────────────
|
||||||
|
isDeleting
|
||||||
|
? const Padding(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2.5),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(Icons.delete_sweep_outlined,
|
||||||
|
color: Colors.redAccent),
|
||||||
|
tooltip: 'Hapus riwayat',
|
||||||
|
onPressed: totalCount == 0 ? null : onDeleteTap,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -601,6 +601,24 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// Delete bottom sheet
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
void _showDeleteSheet(BuildContext context) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
useSafeArea: true,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder: (_) => BlocProvider.value(
|
||||||
|
value: context.read<WindSpeedBloc>(),
|
||||||
|
child: const _DeleteSheet(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -634,7 +652,16 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: BlocBuilder<WindSpeedBloc, WindSpeedState>(
|
body: BlocConsumer<WindSpeedBloc, WindSpeedState>(
|
||||||
|
listenWhen: (prev, curr) =>
|
||||||
|
curr.deleteError != null && prev.deleteError != curr.deleteError,
|
||||||
|
listener: (context, state) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text('Gagal menghapus: ${state.deleteError}'),
|
||||||
|
backgroundColor: Colors.red.shade700,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
));
|
||||||
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
|
@ -876,6 +903,26 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
|
||||||
icon: Icons.calendar_month_rounded,
|
icon: Icons.calendar_month_rounded,
|
||||||
onTap: () => _pickDate(context, state),
|
onTap: () => _pickDate(context, state),
|
||||||
),
|
),
|
||||||
|
// ── Tombol Delete ──────────────────────────────────────────
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
state.isDeleting
|
||||||
|
? const Padding(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2.5),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
icon: const Icon(Icons.delete_sweep_outlined,
|
||||||
|
color: Colors.redAccent),
|
||||||
|
tooltip: 'Hapus riwayat',
|
||||||
|
onPressed: state.history.isEmpty
|
||||||
|
? null
|
||||||
|
: () => _showDeleteSheet(context),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -1129,3 +1176,310 @@ class _HistoryTile extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// Delete Bottom Sheet
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
class _DeleteSheet extends StatefulWidget {
|
||||||
|
const _DeleteSheet();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_DeleteSheet> createState() => _DeleteSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeleteSheetState extends State<_DeleteSheet> {
|
||||||
|
int _mode = 0; // 0=all, 1=date, 2=range, 3=hour
|
||||||
|
DateTime? _byDate;
|
||||||
|
DateTime? _rangeStart;
|
||||||
|
DateTime? _rangeEnd;
|
||||||
|
DateTime? _hourDate;
|
||||||
|
int _startHour = 0;
|
||||||
|
int _endHour = 23;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
16, 16, 16, MediaQuery.of(context).viewInsets.bottom + 24),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(children: [
|
||||||
|
const Icon(Icons.delete_sweep, color: Colors.redAccent),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
const Text('Hapus Riwayat Angin',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () => Navigator.pop(context)),
|
||||||
|
]),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
_modeCard(0, Icons.select_all_rounded, 'Hapus Semua',
|
||||||
|
'Menghapus seluruh riwayat dari Firebase'),
|
||||||
|
_modeCard(1, Icons.event_outlined, 'Berdasarkan Tanggal',
|
||||||
|
'Hapus data pada satu tanggal tertentu'),
|
||||||
|
_modeCard(2, Icons.date_range_outlined, 'Rentang Tanggal',
|
||||||
|
'Hapus data dalam rentang tanggal (inklusif)'),
|
||||||
|
_modeCard(3, Icons.schedule_outlined, 'Rentang Jam',
|
||||||
|
'Hapus data berdasarkan jam pada suatu tanggal'),
|
||||||
|
if (_mode > 0) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: _buildSubInput(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 48,
|
||||||
|
child: FilledButton.icon(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
onPressed: _valid ? _confirm : null,
|
||||||
|
icon: const Icon(Icons.delete_forever),
|
||||||
|
label: const Text('Hapus Sekarang',
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_mode == 0)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 8),
|
||||||
|
child: Center(
|
||||||
|
child: Text('⚠️ Tindakan ini tidak dapat dibatalkan',
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.deepOrange)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _modeCard(int value, IconData icon, String title, String sub) {
|
||||||
|
final sel = _mode == value;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => setState(() => _mode = value),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 3),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: sel ? Colors.red.withOpacity(0.07) : Colors.transparent,
|
||||||
|
border: Border.all(
|
||||||
|
color: sel ? Colors.red.shade300 : Colors.grey.shade200),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
Icon(icon, size: 20, color: sel ? Colors.red : Colors.grey),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child:
|
||||||
|
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 13,
|
||||||
|
color: sel ? Colors.red : Colors.black87)),
|
||||||
|
Text(sub,
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
if (sel) const Icon(Icons.check_circle, color: Colors.red, size: 18),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSubInput() {
|
||||||
|
switch (_mode) {
|
||||||
|
case 1:
|
||||||
|
return _datePicker(
|
||||||
|
'Pilih Tanggal', _byDate, (d) => setState(() => _byDate = d),
|
||||||
|
key: const ValueKey('by_date'));
|
||||||
|
case 2:
|
||||||
|
return Column(key: const ValueKey('range'), children: [
|
||||||
|
_datePicker('Tanggal Mulai', _rangeStart,
|
||||||
|
(d) => setState(() => _rangeStart = d)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_datePicker(
|
||||||
|
'Tanggal Akhir', _rangeEnd, (d) => setState(() => _rangeEnd = d)),
|
||||||
|
if (_rangeStart != null &&
|
||||||
|
_rangeEnd != null &&
|
||||||
|
_rangeEnd!.isBefore(_rangeStart!))
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 4),
|
||||||
|
child: Text('Tanggal akhir harus ≥ tanggal mulai',
|
||||||
|
style: TextStyle(color: Colors.red, fontSize: 11)),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
case 3:
|
||||||
|
return Column(
|
||||||
|
key: const ValueKey('hour'),
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_datePicker('Pilih Tanggal', _hourDate,
|
||||||
|
(d) => setState(() => _hourDate = d)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text('Rentang Jam',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13)),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Row(children: [
|
||||||
|
Expanded(
|
||||||
|
child: _hourDrop('Jam Mulai', _startHour,
|
||||||
|
(v) => setState(() => _startHour = v))),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
child: Text('—', style: TextStyle(fontSize: 18))),
|
||||||
|
Expanded(
|
||||||
|
child: _hourDrop('Jam Akhir', _endHour,
|
||||||
|
(v) => setState(() => _endHour = v))),
|
||||||
|
]),
|
||||||
|
if (_startHour > _endHour)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 4),
|
||||||
|
child: Text('Jam mulai harus ≤ jam akhir',
|
||||||
|
style: TextStyle(color: Colors.red, fontSize: 11)),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
default:
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _datePicker(
|
||||||
|
String hint, DateTime? value, ValueChanged<DateTime> onPick,
|
||||||
|
{Key? key}) {
|
||||||
|
return GestureDetector(
|
||||||
|
key: key,
|
||||||
|
onTap: () async {
|
||||||
|
final d = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: value ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
confirmText: 'Pilih',
|
||||||
|
cancelText: 'Batal',
|
||||||
|
);
|
||||||
|
if (d != null) onPick(d);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade50,
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
value != null ? Colors.red.shade300 : Colors.grey.shade300),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
Icon(Icons.calendar_month_outlined,
|
||||||
|
size: 16,
|
||||||
|
color: value != null ? Colors.red : Colors.grey.shade400),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
value != null
|
||||||
|
? DateFormat('d MMMM yyyy', 'id_ID').format(value)
|
||||||
|
: hint,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: value != null ? Colors.black87 : Colors.grey.shade400),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _hourDrop(String label, int value, ValueChanged<int> onChange) {
|
||||||
|
return DropdownButtonFormField<int>(
|
||||||
|
value: value,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
labelStyle: const TextStyle(fontSize: 11),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
items: List.generate(
|
||||||
|
24,
|
||||||
|
(i) => DropdownMenuItem(
|
||||||
|
value: i,
|
||||||
|
child: Text('${i.toString().padLeft(2, '0')}:00',
|
||||||
|
style: const TextStyle(fontSize: 13)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (v) => onChange(v!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _valid {
|
||||||
|
switch (_mode) {
|
||||||
|
case 0:
|
||||||
|
return true;
|
||||||
|
case 1:
|
||||||
|
return _byDate != null;
|
||||||
|
case 2:
|
||||||
|
return _rangeStart != null &&
|
||||||
|
_rangeEnd != null &&
|
||||||
|
!_rangeEnd!.isBefore(_rangeStart!);
|
||||||
|
case 3:
|
||||||
|
return _hourDate != null && _startHour <= _endHour;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _confirm() {
|
||||||
|
final bloc = context.read<WindSpeedBloc>();
|
||||||
|
if (_mode == 0) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Hapus Semua?'),
|
||||||
|
content:
|
||||||
|
const Text('Seluruh riwayat akan dihapus permanen dari Firebase. '
|
||||||
|
'Tindakan ini tidak dapat dibatalkan.'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: const Text('Batal')),
|
||||||
|
FilledButton(
|
||||||
|
style: FilledButton.styleFrom(backgroundColor: Colors.red),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
Navigator.pop(context);
|
||||||
|
bloc.add(const WindSpeedDeleteAllRequested());
|
||||||
|
},
|
||||||
|
child: const Text('Ya, Hapus Semua'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (_mode) {
|
||||||
|
case 1:
|
||||||
|
bloc.add(WindSpeedDeleteByDateRequested(_byDate!));
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
bloc.add(WindSpeedDeleteByDateRangeRequested(
|
||||||
|
start: _rangeStart!, end: _rangeEnd!));
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
bloc.add(WindSpeedDeleteByHourRangeRequested(
|
||||||
|
date: _hourDate!, startHour: _startHour, endHour: _endHour));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,43 @@ class FirebaseMonitoringRepo implements MonitoringRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, T>> getSensorHistoryWithKeys<T>(
|
||||||
|
String path,
|
||||||
|
T Function(Map<dynamic, dynamic> json) mapper,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final snapshot = await _db.ref(path).get();
|
||||||
|
if (snapshot.exists && snapshot.value is Map) {
|
||||||
|
final data = snapshot.value as Map<dynamic, dynamic>;
|
||||||
|
return {
|
||||||
|
for (final entry in data.entries)
|
||||||
|
entry.key.toString(): mapper(
|
||||||
|
entry.value is Map ? entry.value as Map<dynamic, dynamic> : {},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
} catch (_) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteHistoryByKeys(String path, List<String> keys) async {
|
||||||
|
if (keys.isEmpty) return;
|
||||||
|
// Firebase multi-path delete: set tiap path ke null dalam satu update
|
||||||
|
final Map<String, dynamic> updates = {
|
||||||
|
for (final key in keys) '$path/$key': null,
|
||||||
|
};
|
||||||
|
await _db.ref().update(updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteAllHistory(String path) async {
|
||||||
|
await _db.ref(path).remove();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Anemometer Settings ──────────────────────────────────────
|
// ── Anemometer Settings ──────────────────────────────────────
|
||||||
@override
|
@override
|
||||||
Future<Map<String, dynamic>> getAnemometerSettings() async {
|
Future<Map<String, dynamic>> getAnemometerSettings() async {
|
||||||
|
|
|
||||||
|
|
@ -43,4 +43,13 @@ abstract class MonitoringRepository {
|
||||||
Future<void> sendRemoteRestart(String deviceId);
|
Future<void> sendRemoteRestart(String deviceId);
|
||||||
|
|
||||||
Future<void> deleteDeviceLogs(String deviceId, List<String> keys);
|
Future<void> deleteDeviceLogs(String deviceId, List<String> keys);
|
||||||
|
|
||||||
|
Future<Map<String, T>> getSensorHistoryWithKeys<T>(
|
||||||
|
String path,
|
||||||
|
T Function(Map<dynamic, dynamic> json) mapper,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> deleteHistoryByKeys(String path, List<String> keys);
|
||||||
|
|
||||||
|
Future<void> deleteAllHistory(String path);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue