From 4b9ea8598cc34d0bbcbd58bdd74da3a07a386399 Mon Sep 17 00:00:00 2001 From: Mochamad ongki ramadani Date: Sun, 21 Jun 2026 22:36:32 +0700 Subject: [PATCH] tes --- .../blocs/evaporasi_settings_bloc.dart | 3 - .../views/device_setup_screen.dart | 77 --------- .../evaporasi/blocs/evaporasi_bloc.dart | 155 ++++++++++-------- .../evaporasi/blocs/evaporasi_state.dart | 6 +- .../evaporasi/views/evaporasi_screen.dart | 65 -------- .../widgets/evaporasi_range_selector.dart | 19 ++- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 60 ++++++- pubspec.yaml | 1 + windows/flutter/generated_plugins.cmake | 1 + 10 files changed, 162 insertions(+), 227 deletions(-) diff --git a/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_bloc.dart b/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_bloc.dart index 1d7855a..e170ecc 100644 --- a/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_bloc.dart +++ b/lib/screens/monitoring/device_setup/blocs/evaporasi_settings_bloc.dart @@ -18,7 +18,6 @@ class EvaporasiSettingsBloc extends Bloc(_onStarted); on(_onThresholdRendahChanged); on(_onThresholdTinggiChanged); - // rumusKalibrasi removed: calculation is handled on ESP32 hardware on(_onOffsetChanged); on(_onPumpStartChanged); on(_onPumpEndChanged); @@ -133,8 +132,6 @@ class EvaporasiSettingsBloc extends Bloc emit, ) => emit(state.copyWith(thresholdTinggi: event.value)); - // rumusKalibrasi handling removed — device firmware defines the formula - void _onOffsetChanged( EvaporasiKoreksiOffsetChanged event, Emitter emit, 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 dbd1cc8..4652c55 100644 --- a/lib/screens/monitoring/device_setup/views/device_setup_screen.dart +++ b/lib/screens/monitoring/device_setup/views/device_setup_screen.dart @@ -336,14 +336,6 @@ class _SensorSettingsTab extends StatelessWidget { ), const SizedBox(height: 32), - // ── Rumus preview ───────────────────────────────────── - _FormulaPreview( - kFaktor: state.kFaktor, - radiusM: state.radiusM, - magnetCount: state.magnetCount, - ), - const SizedBox(height: 32), - // ── Tombol Simpan ───────────────────────────────────── SizedBox( width: double.infinity, @@ -1037,75 +1029,6 @@ class _SettingsCard extends StatelessWidget { } } -class _FormulaPreview extends StatelessWidget { - final double kFaktor; - final double radiusM; - final int magnetCount; - - const _FormulaPreview({ - required this.kFaktor, - required this.radiusM, - required this.magnetCount, - }); - - @override - Widget build(BuildContext context) { - // Contoh: 1 pulsa per detik - final exampleRps = 1.0 / magnetCount; - final exampleSpeed = 2 * 3.14159 * radiusM * exampleRps * kFaktor; - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(14), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [ - Icon(Icons.functions_rounded, - size: 16, color: Colors.amber.shade300), - const SizedBox(width: 8), - Text('Preview Rumus', - style: TextStyle( - fontSize: 12, - color: Colors.amber.shade300, - fontWeight: FontWeight.bold)), - ]), - const SizedBox(height: 10), - Text( - 'speed = 2π × radius × (pulsa / (dt × magnet)) × k', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade400, - fontFamily: 'monospace'), - ), - const SizedBox(height: 6), - Text( - ' = 2π × ${radiusM.toStringAsFixed(3)}m × (p / (dt × $magnetCount)) × ${kFaktor.toStringAsFixed(1)}', - style: const TextStyle( - fontSize: 12, color: Colors.white70, fontFamily: 'monospace'), - ), - const SizedBox(height: 10), - Text( - '@ 1 pulsa/detik → ${exampleSpeed.toStringAsFixed(3)} m/s', - style: TextStyle( - fontSize: 13, - color: Colors.green.shade300, - fontWeight: FontWeight.bold, - fontFamily: 'monospace'), - ), - ], - ), - ); - } -} - -// ── WIDGET BARU: _MagnetCountSelector ───────────────────────── -// Tambahkan class baru ini di bagian bawah file, -// setelah class _FormulaPreview - class _MagnetCountSelector extends StatelessWidget { final int selected; final ValueChanged onSelected; diff --git a/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart b/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart index 5293dcd..bd5e955 100644 --- a/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart +++ b/lib/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:firebase_database/firebase_database.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:monitoring_repository/monitoring_repository.dart'; import '../../../../blocs/notification_bloc/notification_bloc.dart'; @@ -56,6 +57,8 @@ class EvaporasiBloc extends Bloc { final MonitoringRepository _repository; final NotificationBloc _notificationBloc; + final FlutterLocalNotificationsPlugin _localNotificationsPlugin = + FlutterLocalNotificationsPlugin(); StreamSubscription? _subscription; double _thresholdRendah = 2.0; double _thresholdTinggi = 10.0; @@ -66,6 +69,7 @@ class EvaporasiBloc extends Bloc { }) : _repository = repository, _notificationBloc = notificationBloc, super(EvaporasiState()) { + _initLocalNotifications(); on(_onStarted); on<_EvaporasiRealtimeUpdated>(_onRealtimeUpdated); on(_onDateRangeChanged); @@ -158,6 +162,17 @@ class EvaporasiBloc extends Bloc { thresholdRendah: _thresholdRendah, thresholdTinggi: _thresholdTinggi, ); + + final bool hasFallenBelowCritical = + event.data.tinggiAir < state.batasKritisCm && + state.waterLevel >= state.batasKritisCm; + final bool statusBecameHigh = + state.weatherStatus != 'Tinggi' && status == 'Tinggi'; + + if (hasFallenBelowCritical || statusBecameHigh) { + await _showCriticalNotification(); + } + _emitAlert(status, willRain, event.data.evaporasi); emit(state.copyWith( @@ -180,79 +195,29 @@ class EvaporasiBloc extends Bloc { emit(state.copyWith(isLoading: true)); final history = state.history; - final start = event.startDate; - final end = event.endDate; - - final isSingle = _isSameDay(start, end); - - List values; - List temps; - List labels; - - - if (isSingle) { - // 1 hari → per jam - values = TimeSeriesMapper.toSpecificDate( - data: history, - getTime: (e) => e.timestamp, - getValue: (e) => e.evaporasi, - targetDate: start, - ); - temps = TimeSeriesMapper.toSpecificDate( - data: history, - getTime: (e) => e.timestamp, - getValue: (e) => e.suhu, - targetDate: start, - ); - labels = List.generate(24, (i) => '${i.toString().padLeft(2, '0')}:00'); - } else { - // Range → per hari (tapi evaporasi dikalibrasi dengan rumus: - // E(hari ke-2) = max(evap hari ke-1) - max(evap hari ke-2)) - final days = _buildDayList(start, end); - - - final dailyMaxEvap = List.filled(days.length, 0.0); - final dailyMaxTemp = List.filled(days.length, 0.0); - final dailyHasValue = List.filled(days.length, false); - - - for (final item in history) { - final d = DateTime(item.timestamp.year, item.timestamp.month, item.timestamp.day); - final idx = days.indexWhere((x) => x == d); - if (idx < 0) continue; - - final evap = item.evaporasi; - final temp = item.suhu; - - if (!dailyHasValue[idx]) { - dailyHasValue[idx] = true; - dailyMaxEvap[idx] = evap; - dailyMaxTemp[idx] = temp; - } else { - if (evap > dailyMaxEvap[idx]) dailyMaxEvap[idx] = evap; - if (temp > dailyMaxTemp[idx]) dailyMaxTemp[idx] = temp; - } - } - - // Hitung E berbasis pasangan H1 (maks hari sebelumnya) - H2 (maks hari ini). - // Definisi sesuai permintaan: E untuk hari i = maxEvap(hari i-1) - maxEvap(hari i) - // Mulai tanggal 21 Mei s/d sekarang: untuk hari pertama di rentang, nilainya 0 - final calibrated = List.filled(days.length, 0.0); - for (int i = 1; i < days.length; i++) { - final e = dailyMaxEvap[i - 1] - dailyMaxEvap[i]; - calibrated[i] = e < 0 ? 0.0 : e; - } - - values = calibrated; - temps = dailyMaxTemp; - labels = _buildLabels(days); - - } + final selectedDate = DateTime( + event.startDate.year, + event.startDate.month, + event.startDate.day, + ); + final values = TimeSeriesMapper.toSpecificDate( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.evaporasi, + targetDate: selectedDate, + ); + final temps = TimeSeriesMapper.toSpecificDate( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.suhu, + targetDate: selectedDate, + ); + final labels = List.generate(24, (i) => '${i.toString().padLeft(2, '0')}:00'); emit(state.copyWith( - startDate: start, - endDate: end, + startDate: selectedDate, + endDate: selectedDate, chartValues: values, chartTemperatures: temps, chartLabels: labels, @@ -317,6 +282,56 @@ class EvaporasiBloc extends Bloc { } } + Future _initLocalNotifications() async { + const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); + const iosSettings = DarwinInitializationSettings(); + const initSettings = InitializationSettings( + android: androidSettings, + iOS: iosSettings, + macOS: iosSettings, + ); + + try { + await _localNotificationsPlugin.initialize(settings: initSettings); + } catch (_) { + // Jika gagal, lanjutkan tanpa notifikasi lokal. + } + } + + Future _showCriticalNotification() async { + const androidDetails = AndroidNotificationDetails( + 'evaporasi_alerts', + 'Evaporasi Alerts', + channelDescription: 'Peringatan Evaporasi dan level air kritis', + importance: Importance.max, + priority: Priority.high, + ticker: 'Peringatan Darurat', + playSound: true, + enableVibration: true, + ); + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentSound: true, + presentBadge: true, + ); + const notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + macOS: iosDetails, + ); + + try { + await _localNotificationsPlugin.show( + id: 0, + title: 'Peringatan Darurat', + body: 'Peringatan Darurat: Batas Air Kritis tercapai, periksa pompa segera!', + notificationDetails: notificationDetails, + ); + } catch (_) { + // Ignore local notification failure. + } + } + static double _toDouble(dynamic value, double fallback) { if (value == null) return fallback; if (value is num) return value.toDouble(); diff --git a/lib/screens/monitoring/evaporasi/blocs/evaporasi_state.dart b/lib/screens/monitoring/evaporasi/blocs/evaporasi_state.dart index 5a3486d..277416b 100644 --- a/lib/screens/monitoring/evaporasi/blocs/evaporasi_state.dart +++ b/lib/screens/monitoring/evaporasi/blocs/evaporasi_state.dart @@ -5,6 +5,7 @@ class EvaporasiState extends Equatable { final double currentValue; final double temperature; final double waterLevel; + final double batasKritisCm; // Rentang tanggal aktif untuk grafik final DateTime startDate; @@ -31,6 +32,7 @@ class EvaporasiState extends Equatable { this.currentValue = 0.0, this.temperature = 0.0, this.waterLevel = 0.0, + this.batasKritisCm = 15.0, DateTime? startDate, DateTime? endDate, this.chartValues = const [], @@ -50,6 +52,7 @@ class EvaporasiState extends Equatable { double? currentValue, double? temperature, double? waterLevel, + double? batasKritisCm, DateTime? startDate, DateTime? endDate, List? chartValues, @@ -68,6 +71,7 @@ class EvaporasiState extends Equatable { currentValue: currentValue ?? this.currentValue, temperature: temperature ?? this.temperature, waterLevel: waterLevel ?? this.waterLevel, + batasKritisCm: batasKritisCm ?? this.batasKritisCm, startDate: startDate ?? this.startDate, endDate: endDate ?? this.endDate, chartValues: chartValues ?? this.chartValues, @@ -94,7 +98,7 @@ class EvaporasiState extends Equatable { @override List get props => [ - currentValue, temperature, waterLevel, + currentValue, temperature, waterLevel, batasKritisCm, startDate, endDate, chartValues, chartTemperatures, chartLabels, history, filteredHistory, selectedDateFilter, diff --git a/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart b/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart index bc98321..2cb0d24 100644 --- a/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart +++ b/lib/screens/monitoring/evaporasi/views/evaporasi_screen.dart @@ -372,71 +372,6 @@ class _EvaporasiScreenState extends State { const EvaporasiRangeSelector(), const SizedBox(height: 12), - // ── Block nilai E (kalibrasi H1 - H2) ────────────────── - BlocBuilder( - builder: (context, s) { - if (s.chartValues.isEmpty) { - return const SizedBox.shrink(); - } - - // Untuk range > 1 hari, chartValues berisi E per hari - final isRange = !s.isSingleDay; - if (!isRange) { - return const SizedBox.shrink(); - } - - // Tampilkan E pada hari terakhir rentang (hari ke-N) - final eLast = s.chartValues.last; - - return Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - border: Border.all(color: Colors.blue.shade100), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: const [ - Icon(Icons.calculate_rounded, - color: Colors.blue, size: 20), - SizedBox(width: 10), - Text( - 'Nilai Evaporasi Terkalibrasi (E)', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - 'E terakhir (hari terakhir rentang): ${eLast.toStringAsFixed(2)} mm', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.blue.shade700, - ), - ), - const SizedBox(height: 6), - Text( - 'Rumus: E = max(H1) − max(H2)', - style: TextStyle( - fontSize: 12, - color: Colors.blueGrey.shade700, - ), - ), - ], - ), - ); - }, - ), - const SizedBox(height: 20), - // ── Chart ─────────────────────────────────── BlocBuilder( builder: (context, s) => EvaporasiChartWidget( diff --git a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_range_selector.dart b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_range_selector.dart index 095e934..819248d 100644 --- a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_range_selector.dart +++ b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_range_selector.dart @@ -20,7 +20,7 @@ class EvaporasiRangeSelector extends StatelessWidget { : '${_fmt(start)} → ${_fmt(end)}'; return GestureDetector( - onTap: () => _pickRange(context, start, end), + onTap: () => _pickDate(context, start), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( @@ -79,9 +79,9 @@ class EvaporasiRangeSelector extends StatelessWidget { }, ), const SizedBox(width: 6), - // Tombol pilih range bebas + // Tombol pilih tanggal harian GestureDetector( - onTap: () => _pickRange(context, start, end), + onTap: () => _pickDate(context, start), child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), @@ -115,13 +115,13 @@ class EvaporasiRangeSelector extends StatelessWidget { ); } - Future _pickRange( - BuildContext context, DateTime start, DateTime end) async { - final picked = await showDateRangePicker( + Future _pickDate( + BuildContext context, DateTime date) async { + final picked = await showDatePicker( context: context, + initialDate: date, firstDate: DateTime(2024), lastDate: DateTime.now(), - initialDateRange: DateTimeRange(start: start, end: end), locale: const Locale('id', 'ID'), builder: (context, child) { return Theme( @@ -138,10 +138,11 @@ class EvaporasiRangeSelector extends StatelessWidget { ); if (picked != null && context.mounted) { + final selected = DateTime(picked.year, picked.month, picked.day); context.read().add( EvaporasiDateRangeChanged( - startDate: picked.start, - endDate: picked.end, + startDate: selected, + endDate: selected, ), ); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index acc9d4a..fd0b429 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import cloud_firestore import firebase_auth import firebase_core import firebase_database +import flutter_local_notifications import flutter_localization import google_sign_in_ios import printing @@ -22,6 +23,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin")) FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) diff --git a/pubspec.lock b/pubspec.lock index fabfa53..0897cf4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -193,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dbus: + dependency: transitive + description: + name: dbus + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + url: "https://pub.dev" + source: hosted + version: "0.7.14" dio: dependency: "direct main" description: @@ -374,6 +382,46 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: a0d7141f14cabcee42967470a858dfc99dd6cfb70d3cab404bacfcafa9e84e70 + url: "https://pub.dev" + source: hosted + version: "22.0.1" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "9ca97e63776f29ab1b955725c09999fc2c150523269db150c39274f2a43c5a8b" + url: "https://pub.dev" + source: hosted + version: "8.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: ff0013eae795e8dc8fad4a8992a209e64d3ba2fbd8bf5e43c36bf448f95bd814 + url: "https://pub.dev" + source: hosted + version: "12.0.0" + flutter_local_notifications_web: + dependency: transitive + description: + name: flutter_local_notifications_web + sha256: "516afaf97a2d1e67a036c6617321b00d205d72f7a67b6eccf936cd565f985878" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: "6f43bdd03b171b7a90f22647506fea33e2bb12294b7c7c7a3d690e960a382945" + url: "https://pub.dev" + source: hosted + version: "3.1.1" flutter_localization: dependency: "direct main" description: @@ -945,6 +993,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.7" + timezone: + dependency: transitive + description: + name: timezone + sha256: "784a5e34d2eb62e1326f24d6f600aaaee452eb8ca8ef2f384a59244e292d158b" + url: "https://pub.dev" + source: hosted + version: "0.11.0" typed_data: dependency: transitive description: @@ -1036,10 +1092,10 @@ packages: dependency: transitive description: name: win32 - sha256: a1fc9eb9248baa05dfc12ed5b66e377b3e23f095eec078e0371622b9033810d9 + sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a05956a..c079de8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: path: packages/monitoring_repository path_provider: ^2.1.5 pdf: ^3.11.1 + flutter_local_notifications: ^22.0.1 printing: ^5.13.1 rxdart: ^0.27.7 share_plus: ^13.1.0 diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 32a0f2c..33e1bdf 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -13,6 +13,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_local_notifications_windows jni )