From f9a74b9b4e1a88d4cd9f3132ae747f098c827317 Mon Sep 17 00:00:00 2001 From: kleponijo Date: Fri, 29 May 2026 19:05:16 +0700 Subject: [PATCH] update delete log bisa select dan hapus log --- .../device_setup/blocs/device_setup_bloc.dart | 72 ++++++ .../blocs/device_setup_event.dart | 11 + .../blocs/device_setup_state.dart | 15 +- .../views/device_setup_screen.dart | 221 +++++++++++++++--- .../lib/src/firebase_monitoring_repo.dart | 10 + .../lib/src/monitoring_repo.dart | 2 + 6 files changed, 289 insertions(+), 42 deletions(-) diff --git a/lib/screens/monitoring/device_setup/blocs/device_setup_bloc.dart b/lib/screens/monitoring/device_setup/blocs/device_setup_bloc.dart index 65c71b3..d22291b 100644 --- a/lib/screens/monitoring/device_setup/blocs/device_setup_bloc.dart +++ b/lib/screens/monitoring/device_setup/blocs/device_setup_bloc.dart @@ -37,6 +37,10 @@ class DeviceSetupBloc extends Bloc { on(_onSettingsSaved); on(_onLogsRefreshed); on(_onRestartRequested); + on(_onSelectModeToggled); + on(_onLogItemToggled); + on(_onLogSelectAllToggled); + on(_onLogsDeleteRequested); } // ════════════════════════════════════════════════════════════ @@ -256,6 +260,74 @@ class DeviceSetupBloc extends Bloc { } } + void _onSelectModeToggled( + LogSelectModeToggled event, + Emitter emit, + ) { + emit(state.copyWith( + isSelecting: !state.isSelecting, + selectedLogKeys: {}, // reset pilihan saat toggle mode + )); + } + + void _onLogItemToggled( + LogItemToggled event, + Emitter emit, + ) { + final current = Set.from(state.selectedLogKeys); + if (current.contains(event.key)) { + current.remove(event.key); + } else { + current.add(event.key); + } + emit(state.copyWith(selectedLogKeys: current)); + } + + void _onLogSelectAllToggled( + LogSelectAllToggled event, + Emitter emit, + ) { + if (state.allSelected) { + // Sudah semua terpilih → batalkan semua + emit(state.copyWith(selectedLogKeys: {})); + } else { + // Pilih semua — ambil semua '_key' dari logs + final allKeys = state.logs.map((e) => e['_key'] as String).toSet(); + emit(state.copyWith(selectedLogKeys: allKeys)); + } + } + + Future _onLogsDeleteRequested( + LogsDeleteRequested event, + Emitter emit, + ) async { + if (state.selectedLogKeys.isEmpty) return; + + emit(state.copyWith(logsLoading: true)); + + try { + await _repository.deleteDeviceLogs( + state.deviceId, + state.selectedLogKeys.toList(), + ); + + // Refresh logs setelah delete + final logs = await _repository.getDeviceLogs(state.deviceId); + emit(state.copyWith( + logs: logs, + logsLoading: false, + selectedLogKeys: {}, + isSelecting: false, + )); + } catch (e) { + emit(state.copyWith( + logsLoading: false, + status: DeviceSetupStatus.settingsError, + errorMessage: 'Gagal hapus log: $e', + )); + } + } + // ── Reset state ─────────────────────────────────────────────── void _onReset( ResetDeviceSetupEvent event, diff --git a/lib/screens/monitoring/device_setup/blocs/device_setup_event.dart b/lib/screens/monitoring/device_setup/blocs/device_setup_event.dart index e7c9df8..10d103d 100644 --- a/lib/screens/monitoring/device_setup/blocs/device_setup_event.dart +++ b/lib/screens/monitoring/device_setup/blocs/device_setup_event.dart @@ -50,3 +50,14 @@ class DeviceSettingsSaved extends DeviceSetupEvent {} class DeviceLogsRefreshed extends DeviceSetupEvent {} class DeviceRestartRequested extends DeviceSetupEvent {} + +class LogSelectModeToggled extends DeviceSetupEvent {} + +class LogItemToggled extends DeviceSetupEvent { + final String key; + LogItemToggled(this.key); +} + +class LogSelectAllToggled extends DeviceSetupEvent {} + +class LogsDeleteRequested extends DeviceSetupEvent {} diff --git a/lib/screens/monitoring/device_setup/blocs/device_setup_state.dart b/lib/screens/monitoring/device_setup/blocs/device_setup_state.dart index 86d29c1..55fa1cc 100644 --- a/lib/screens/monitoring/device_setup/blocs/device_setup_state.dart +++ b/lib/screens/monitoring/device_setup/blocs/device_setup_state.dart @@ -26,13 +26,11 @@ class DeviceSetupState { final double radiusM; final int intervalRealtimeMs; final int intervalHistoryMs; - - /// Jumlah magnet pada anemometer. 1 = default, 3 = resolusi lebih tinggi. - /// Disimpan di Firebase: /anemometer/settings/magnet_count final int magnetCount; - final List> logs; final bool logsLoading; + final Set selectedLogKeys; + final bool isSelecting; const DeviceSetupState({ this.status = DeviceSetupStatus.idle, @@ -47,8 +45,13 @@ class DeviceSetupState { this.magnetCount = 1, // ← default 1 magnet this.logs = const [], this.logsLoading = false, + this.selectedLogKeys = const {}, + this.isSelecting = false, }); + bool get allSelected => + logs.isNotEmpty && selectedLogKeys.length == logs.length; + DeviceSetupState copyWith({ DeviceSetupStatus? status, String? errorMessage, @@ -62,6 +65,8 @@ class DeviceSetupState { int? magnetCount, List>? logs, bool? logsLoading, + Set? selectedLogKeys, + bool? isSelecting, }) { return DeviceSetupState( status: status ?? this.status, @@ -76,6 +81,8 @@ class DeviceSetupState { magnetCount: magnetCount ?? this.magnetCount, logs: logs ?? this.logs, logsLoading: logsLoading ?? this.logsLoading, + selectedLogKeys: selectedLogKeys ?? this.selectedLogKeys, + isSelecting: isSelecting ?? this.isSelecting, ); } } diff --git a/lib/screens/monitoring/device_setup/views/device_setup_screen.dart b/lib/screens/monitoring/device_setup/views/device_setup_screen.dart index 8b5f1f2..dbd1cc8 100644 --- a/lib/screens/monitoring/device_setup/views/device_setup_screen.dart +++ b/lib/screens/monitoring/device_setup/views/device_setup_screen.dart @@ -581,10 +581,51 @@ class _LogsTab extends StatelessWidget { ); } + void _showDeleteConfirmDialog(BuildContext context, DeviceSetupBloc bloc) { + final count = state.selectedLogKeys.length; + showDialog( + context: context, + builder: (ctx) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + title: Row(children: [ + Icon(Icons.delete_outline_rounded, color: Colors.red.shade600), + const SizedBox(width: 8), + const Text('Hapus Log'), + ]), + content: Text( + 'Hapus $count log yang dipilih?\n\n' + 'Data akan dihapus permanen dari Firebase dan tidak bisa dikembalikan.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text('Batal', style: TextStyle(color: Colors.grey.shade600)), + ), + ElevatedButton.icon( + onPressed: () { + Navigator.pop(ctx); + bloc.add(LogsDeleteRequested()); + }, + icon: const Icon(Icons.delete_rounded, size: 16), + label: const Text('Hapus'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade600, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final bloc = context.read(); final fmt = DateFormat('dd MMM HH:mm:ss', 'id_ID'); + final isSelecting = state.isSelecting; + final selectedCount = state.selectedLogKeys.length; return Column( children: [ @@ -597,9 +638,15 @@ class _LogsTab extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Log Device', - style: const TextStyle( - fontSize: 15, fontWeight: FontWeight.bold)), + Text( + isSelecting ? '$selectedCount dipilih' : 'Log Device', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: + isSelecting ? Colors.blue.shade700 : Colors.black87, + ), + ), Text( state.deviceId, style: @@ -609,6 +656,53 @@ class _LogsTab extends StatelessWidget { ), ), + if (isSelecting) ...[ + // Pilih semua + TextButton( + onPressed: () => bloc.add(LogSelectAllToggled()), + child: Text( + state.allSelected ? 'Batal semua' : 'Pilih semua', + style: TextStyle(fontSize: 12, color: Colors.blue.shade700), + ), + ), + // Hapus terpilih + IconButton.filledTonal( + onPressed: selectedCount == 0 + ? null + : () => _showDeleteConfirmDialog(context, bloc), + icon: const Icon(Icons.delete_rounded), + tooltip: 'Hapus yang dipilih', + style: IconButton.styleFrom( + backgroundColor: selectedCount > 0 + ? Colors.red.shade50 + : Colors.grey.shade100, + foregroundColor: selectedCount > 0 + ? Colors.red.shade700 + : Colors.grey.shade400, + ), + ), + // Batalkan mode pilih + IconButton( + onPressed: () => bloc.add(LogSelectModeToggled()), + icon: const Icon(Icons.close_rounded), + tooltip: 'Batalkan', + ), + ] else ...[ + // Tombol pilih / select mode + IconButton.filledTonal( + onPressed: state.logs.isEmpty + ? null + : () => bloc.add(LogSelectModeToggled()), + icon: const Icon(Icons.checklist_rounded), + tooltip: 'Pilih log', + style: IconButton.styleFrom( + backgroundColor: Colors.blue.shade50, + foregroundColor: Colors.blue.shade700, + ), + ), + const SizedBox(width: 4), + ], + // restart esp IconButton.filledTonal( onPressed: () => @@ -663,8 +757,10 @@ class _LogsTab extends StatelessWidget { itemCount: state.logs.length, itemBuilder: (ctx, i) { final log = state.logs[i]; + final key = log['_key'] as String; final msg = log['msg'] as String; final ts = log['timestamp'] as DateTime; + final isSelected = state.selectedLogKeys.contains(key); // Warna berdasarkan konten pesan final isOta = msg.contains('OTA') || msg.contains('FW='); @@ -683,46 +779,95 @@ class _LogsTab extends StatelessWidget { ? Colors.blue.shade50 : Colors.green.shade50; - return Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: bg, - borderRadius: BorderRadius.circular(10), - border: - Border.all(color: color.withValues(alpha: 0.25)), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - isError - ? Icons.error_outline_rounded - : isOta - ? Icons.system_update_rounded - : Icons.check_circle_outline_rounded, - size: 16, - color: color, + return GestureDetector( + // Long press → masuk mode pilih sekaligus pilih item ini + onLongPress: isSelecting + ? null + : () { + bloc.add(LogSelectModeToggled()); + bloc.add(LogItemToggled(key)); + }, + onTap: isSelecting + ? () => bloc.add(LogItemToggled(key)) + : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isSelected ? Colors.blue.shade100 : bg, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: isSelected + ? Colors.blue.shade400 + : color.withValues(alpha: 0.25), + width: isSelected ? 1.5 : 1, ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(msg, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Checkbox atau ikon + if (isSelecting) + Padding( + padding: + const EdgeInsets.only(right: 10, top: 1), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 150), + child: isSelected + ? Icon(Icons.check_circle_rounded, + key: const ValueKey('checked'), + size: 20, + color: Colors.blue.shade700) + : Icon( + Icons.radio_button_unchecked_rounded, + key: const ValueKey('unchecked'), + size: 20, + color: Colors.grey.shade400), + ), + ) + else + Padding( + padding: + const EdgeInsets.only(right: 10, top: 1), + child: Icon( + isError + ? Icons.error_outline_rounded + : isOta + ? Icons.system_update_rounded + : Icons.check_circle_outline_rounded, + size: 16, + color: color, + ), + ), + + // Konten + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + msg, style: TextStyle( - fontSize: 12, - color: color, - fontFamily: 'monospace')), - const SizedBox(height: 2), - Text(fmt.format(ts), + fontSize: 12, + color: isSelected + ? Colors.blue.shade900 + : color, + fontFamily: 'monospace', + ), + ), + const SizedBox(height: 2), + Text( + fmt.format(ts), style: TextStyle( fontSize: 10, - color: Colors.grey.shade500)), - ], + color: Colors.grey.shade500), + ), + ], + ), ), - ), - ], + ], + ), ), ); }, diff --git a/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart b/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart index c8de923..b7566fe 100644 --- a/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart +++ b/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart @@ -129,6 +129,7 @@ class FirebaseMonitoringRepo implements MonitoringRepository { final list = raw.entries.map((e) { final v = e.value as Map; return { + '_key': e.key.toString(), 'msg': v['msg'] ?? '', 'timestamp': DateTime.fromMillisecondsSinceEpoch( (v['timestamp'] ?? 0) * 1000, @@ -146,6 +147,15 @@ class FirebaseMonitoringRepo implements MonitoringRepository { return []; } + @override + Future deleteDeviceLogs(String deviceId, List keys) async { + // Hapus semua key sekaligus pakai Map null (multi-path delete) + final Map updates = { + for (final key in keys) 'anemometer/$deviceId/logs/$key': null, + }; + await _db.ref().update(updates); + } + @override Future sendRemoteRestart(String deviceId) async { await _db.ref('anemometer/$deviceId/command/restart').set(true); diff --git a/packages/monitoring_repository/lib/src/monitoring_repo.dart b/packages/monitoring_repository/lib/src/monitoring_repo.dart index 760b54d..4d495db 100644 --- a/packages/monitoring_repository/lib/src/monitoring_repo.dart +++ b/packages/monitoring_repository/lib/src/monitoring_repo.dart @@ -41,4 +41,6 @@ abstract class MonitoringRepository { }); Future sendRemoteRestart(String deviceId); + + Future deleteDeviceLogs(String deviceId, List keys); }