From d19c5fcaf8f28fed989923ac8febded0576f3675 Mon Sep 17 00:00:00 2001 From: kleponijo <158453666+kleponijo@users.noreply.github.com> Date: Sat, 2 May 2026 10:46:03 +0700 Subject: [PATCH 1/4] oke --- lib/screens/home/widgets/sensor_grid.dart | 77 ----------------------- 1 file changed, 77 deletions(-) diff --git a/lib/screens/home/widgets/sensor_grid.dart b/lib/screens/home/widgets/sensor_grid.dart index 86500cd..83ea239 100644 --- a/lib/screens/home/widgets/sensor_grid.dart +++ b/lib/screens/home/widgets/sensor_grid.dart @@ -50,68 +50,6 @@ class SensorGrid extends StatelessWidget { ), ), - // --- SUHU AIR (dari Evaporasi) --- - BlocBuilder( - builder: (context, state) => SensorCard( - width: cardWidth, - icon: Icons.thermostat_outlined, - iconColor: Colors.orange.shade600, - iconBgColor: Colors.orange.shade50, - label: 'Suhu Air', - value: state.isLoading - ? '—' - : state.temperature.toStringAsFixed(1), - unit: '°C', - isLoading: state.isLoading, - ), - ), - - // --- TINGGI AIR (dari Evaporasi) --- - BlocBuilder( - builder: (context, state) => SensorCard( - width: cardWidth, - icon: Icons.straighten_outlined, - iconColor: Colors.cyan.shade600, - iconBgColor: Colors.cyan.shade50, - label: 'Tinggi Air', - value: - state.isLoading ? '—' : state.waterLevel.toStringAsFixed(1), - unit: 'cm', - isLoading: state.isLoading, - ), - ), - - // --- SUHU UDARA (dari Atmospheric) --- - BlocBuilder( - builder: (context, state) => SensorCard( - width: cardWidth, - icon: Icons.device_thermostat, - iconColor: Colors.red.shade400, - iconBgColor: Colors.red.shade50, - label: 'Suhu Udara', - value: state.isLoading - ? '—' - : state.temperature.toStringAsFixed(1), - unit: '°C', - isLoading: state.isLoading, - ), - ), - - // --- KELEMBAPAN --- - BlocBuilder( - builder: (context, state) => SensorCard( - width: cardWidth, - icon: Icons.water_outlined, - iconColor: Colors.indigo.shade400, - iconBgColor: Colors.indigo.shade50, - label: 'Kelembapan', - value: - state.isLoading ? '—' : state.humidity.toStringAsFixed(1), - unit: '%', - isLoading: state.isLoading, - ), - ), - // --- TEKANAN UDARA --- BlocBuilder( builder: (context, state) => SensorCard( @@ -126,21 +64,6 @@ class SensorGrid extends StatelessWidget { isLoading: state.isLoading, ), ), - - // --- KETINGGIAN --- - BlocBuilder( - builder: (context, state) => SensorCard( - width: cardWidth, - icon: Icons.terrain_outlined, - iconColor: Colors.green.shade600, - iconBgColor: Colors.green.shade50, - label: 'Ketinggian', - value: - state.isLoading ? '—' : state.altitude.toStringAsFixed(1), - unit: 'm', - isLoading: state.isLoading, - ), - ), ], ); }, From 47fc79455ba017b2b6dde67c23413ea400770bf0 Mon Sep 17 00:00:00 2001 From: kleponijo <158453666+kleponijo@users.noreply.github.com> Date: Sat, 2 May 2026 14:22:34 +0700 Subject: [PATCH 2/4] looooo --- lib/screens/home/widgets/sensor_grid.dart | 2 +- .../wind_speed/blocs/wind_speed_bloc.dart | 21 +++++++++++-------- .../wind_speed/blocs/wind_speed_state.dart | 15 +++++++++++-- .../wind_speed/views/wind_speed_screen.dart | 11 +++++++--- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/lib/screens/home/widgets/sensor_grid.dart b/lib/screens/home/widgets/sensor_grid.dart index 83ea239..0f0b56b 100644 --- a/lib/screens/home/widgets/sensor_grid.dart +++ b/lib/screens/home/widgets/sensor_grid.dart @@ -104,7 +104,7 @@ class SensorCard extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 8, offset: const Offset(0, 2), ), diff --git a/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart index 0b7fbed..062787d 100644 --- a/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart +++ b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart @@ -50,15 +50,8 @@ class WindSpeedBloc extends Bloc { (json) => MyWindSpeed.fromJson(json), ); - /// 2. Mapping default (harian) - final raw = TimeSeriesMapper.toDaily( - data: history, - getTime: (e) => e.timestamp, - getValue: (e) => e.speed, - ); - final dailyGraph = TimeSeriesMapper.smooth(raw); - - final daily = TimeSeriesMapper.smooth( + // ✅ Ini saja yang benar + final dailyGraph = TimeSeriesMapper.smooth( TimeSeriesMapper.toDaily( data: history, getTime: (e) => e.timestamp, @@ -93,6 +86,9 @@ class WindSpeedBloc extends Bloc { weeklySpeeds: weekly, monthlySpeeds: monthly, isLoading: false, + alertLevel: history.isNotEmpty // ← tambah ini + ? _getAlertLevel(history.last.speed) + : "Normal", )); /// 3. Start realtime stream (manual subscription) @@ -138,6 +134,7 @@ class WindSpeedBloc extends Bloc { emit(state.copyWith( currentSpeed: newValue, dailySpeeds: updated, + alertLevel: _getAlertLevel(newValue), )); } @@ -194,6 +191,12 @@ class WindSpeedBloc extends Bloc { return super.close(); } + String _getAlertLevel(double speed) { + if (speed >= _kWindDanger) return "Bahaya"; + if (speed >= _kWindWarning) return "Waspada"; + return "Normal"; + } + // ========================= // HELPER: kirim alert ke NotificationBloc // ========================= diff --git a/lib/screens/monitoring/wind_speed/blocs/wind_speed_state.dart b/lib/screens/monitoring/wind_speed/blocs/wind_speed_state.dart index 3276997..f61404d 100644 --- a/lib/screens/monitoring/wind_speed/blocs/wind_speed_state.dart +++ b/lib/screens/monitoring/wind_speed/blocs/wind_speed_state.dart @@ -8,6 +8,7 @@ class WindSpeedState extends Equatable { final List monthlySpeeds; final bool isLoading; final List history; + final String alertLevel; // "Normal" | "Waspada" | "Bahaya" const WindSpeedState({ this.currentSpeed = 0.0, @@ -17,6 +18,7 @@ class WindSpeedState extends Equatable { this.history = const [], this.monthlySpeeds = const [], this.weeklySpeeds = const [], + this.alertLevel = "Normal", }); WindSpeedState copyWith({ @@ -27,6 +29,7 @@ class WindSpeedState extends Equatable { List? monthlySpeeds, bool? isLoading, List? history, + String? alertLevel, }) { return WindSpeedState( currentSpeed: currentSpeed ?? this.currentSpeed, @@ -36,10 +39,18 @@ class WindSpeedState extends Equatable { monthlySpeeds: monthlySpeeds ?? this.monthlySpeeds, isLoading: isLoading ?? this.isLoading, history: history ?? this.history, + alertLevel: alertLevel ?? this.alertLevel, ); } @override - List get props => - [currentSpeed, selectedPeriod, dailySpeeds, isLoading, history]; + List get props => [ + currentSpeed, + selectedPeriod, + dailySpeeds, + weeklySpeeds, + monthlySpeeds, + isLoading, + history + ]; } diff --git a/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart b/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart index 2025e10..f741f4c 100644 --- a/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart +++ b/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart @@ -85,7 +85,7 @@ class WindSpeedScreen extends StatelessWidget { const SizedBox(height: 30), // 3. Info Tambahan (Status/Periode) - _buildDetailRow(state.selectedPeriod), + _buildDetailRow(state.selectedPeriod, state.alertLevel), const SizedBox(height: 24), // ← Tombol Export PDF @@ -143,11 +143,16 @@ class WindSpeedScreen extends StatelessWidget { ); } - Widget _buildDetailRow(String period) { + Widget _buildDetailRow(String period, String alertLevel) { + final (icon, color) = switch (alertLevel) { + "Bahaya" => (Icons.warning_rounded, Colors.red), + "Waspada" => (Icons.info_outline, Colors.orange), + _ => (Icons.check_circle, Colors.green), + }; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _miniInfoCard("Status", "Normal", Icons.check_circle, Colors.green), + _miniInfoCard("Status", alertLevel, icon, color), _miniInfoCard("Periode", period, Icons.timer, Colors.orange), ], ); From 07d5cb369a6d1e227337c3319088138f6483c085 Mon Sep 17 00:00:00 2001 From: kleponijo <158453666+kleponijo@users.noreply.github.com> Date: Sat, 2 May 2026 15:07:33 +0700 Subject: [PATCH 3/4] joss jiss --- .../device_setup/blocs/device_setup_bloc.dart | 3 ++- .../views/device_setup_screen.dart | 26 +++++++++++-------- .../wind_speed/blocs/wind_speed_bloc.dart | 4 +-- .../wind_speed/blocs/wind_speed_event.dart | 8 ------ .../wind_speed/blocs/wind_speed_state.dart | 3 ++- macos/Flutter/GeneratedPluginRegistrant.swift | 2 ++ pubspec.lock | 8 ++++++ pubspec.yaml | 1 + 8 files changed, 32 insertions(+), 23 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 8043ac7..5a42d4c 100644 --- a/lib/screens/monitoring/device_setup/blocs/device_setup_bloc.dart +++ b/lib/screens/monitoring/device_setup/blocs/device_setup_bloc.dart @@ -115,7 +115,8 @@ class DeviceSetupBloc extends Bloc { // ESP restart setelah terima kredensial → koneksi putus → itu normal! // DioException di sini kemungkinan besar karena ESP sudah restart. if (e.type == DioExceptionType.receiveTimeout || - e.type == DioExceptionType.connectionError) { + e.type == DioExceptionType.connectionError || + e.type == DioExceptionType.sendTimeout) { emit(state.copyWith( status: DeviceSetupStatus.success, successMessage: 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 b6a4811..ada34d6 100644 --- a/lib/screens/monitoring/device_setup/views/device_setup_screen.dart +++ b/lib/screens/monitoring/device_setup/views/device_setup_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../blocs/device_setup_bloc.dart'; +import 'package:app_settings/app_settings.dart'; class DeviceSetupScreen extends StatefulWidget { const DeviceSetupScreen({super.key}); @@ -159,7 +160,7 @@ class _DeviceSetupScreenState extends State { showDialog( context: context, barrierDismissible: false, - builder: (_) => AlertDialog( + builder: (dialogContext) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: Row( children: [ @@ -173,12 +174,21 @@ class _DeviceSetupScreenState extends State { ), content: Text(message), actions: [ + if (!success) // ← tambah tombol Coba Lagi jika gagal + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); // tutup dialog + context.read().add( + ResetDeviceSetupEvent()); // kembali ke wind_speed_screen + }, + child: const Text('Coba Lagi'), + ), TextButton( onPressed: () { - Navigator.of(context).pop(); // tutup dialog - Navigator.of(context).pop(); // kembali ke wind_speed_screen + Navigator.of(dialogContext).pop(); + Navigator.of(context).pop(); }, - child: const Text('Selesai'), + child: Text(success ? 'Selesai' : 'Tutup'), ), ], ), @@ -353,13 +363,7 @@ class _OpenWifiSettingsButton extends StatelessWidget { // Buka pengaturan WiFi sistem HP // Butuh package: app_settings (opsional) // AppSettings.openWIFISettings(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Buka Pengaturan WiFi HP → sambungkan ke "Anemometer-Setup"'), - behavior: SnackBarBehavior.floating, - ), - ); + AppSettings.openAppSettings(type: AppSettingsType.wifi); }, icon: const Icon(Icons.settings_rounded, size: 16), label: const Text('Buka Setting'), diff --git a/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart index 062787d..9a995a7 100644 --- a/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart +++ b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart @@ -11,8 +11,8 @@ part 'wind_speed_state.dart'; /// Threshold kecepatan angin (satuan m/s) /// Referensi: Beaufort scale & BMKG -const double _kWindWarning = 2.0; // Waspada: 8–12.5 m/s (~29–45 km/h) -const double _kWindDanger = 4.0; // Bahaya: > 12.5 m/s (> 45 km/h) +const double _kWindWarning = 8.0; // Waspada: 8–12.5 m/s (~29–45 km/h) +const double _kWindDanger = 12.5; // Bahaya: > 12.5 m/s (> 45 km/h) class WindSpeedBloc extends Bloc { final MonitoringRepository _repository; diff --git a/lib/screens/monitoring/wind_speed/blocs/wind_speed_event.dart b/lib/screens/monitoring/wind_speed/blocs/wind_speed_event.dart index 445c8c6..e966119 100644 --- a/lib/screens/monitoring/wind_speed/blocs/wind_speed_event.dart +++ b/lib/screens/monitoring/wind_speed/blocs/wind_speed_event.dart @@ -14,11 +14,3 @@ class WindSpeedPeriodChanged extends WindSpeedEvent { final String period; const WindSpeedPeriodChanged(this.period); } - -// Event internal: saat data baru masuk dari Firebase -class _WindSpeedUpdated extends WindSpeedEvent { - final MyWindSpeed data; - const _WindSpeedUpdated(this.data); - @override - List get props => [data]; -} diff --git a/lib/screens/monitoring/wind_speed/blocs/wind_speed_state.dart b/lib/screens/monitoring/wind_speed/blocs/wind_speed_state.dart index f61404d..7ff5572 100644 --- a/lib/screens/monitoring/wind_speed/blocs/wind_speed_state.dart +++ b/lib/screens/monitoring/wind_speed/blocs/wind_speed_state.dart @@ -51,6 +51,7 @@ class WindSpeedState extends Equatable { weeklySpeeds, monthlySpeeds, isLoading, - history + history, + alertLevel, ]; } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index bf298b9..9b58f8a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import app_settings import cloud_firestore import firebase_auth import firebase_core @@ -13,6 +14,7 @@ import google_sign_in_ios import printing func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppSettingsPlugin.register(with: registry.registrar(forPlugin: "AppSettingsPlugin")) FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) diff --git a/pubspec.lock b/pubspec.lock index ffc5bd8..4867820 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.66" + app_settings: + dependency: "direct main" + description: + name: app_settings + sha256: "64d50e666fd96ae90301bf71205f05019286f940ad6f5fed3d1be19c6af7546a" + url: "https://pub.dev" + source: hosted + version: "7.0.0" archive: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4a46cde..fff0541 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,6 +5,7 @@ version: 1.0.0+1 environment: sdk: ^3.1.0 dependencies: + app_settings: ^7.0.0 bloc: ^8.1.0 bloc_concurrency: ^0.2.5 cloud_firestore: ^6.1.1 From 9ef8baa9b9986b437981d535f38f69d41be59c3a Mon Sep 17 00:00:00 2001 From: kleponijo <158453666+kleponijo@users.noreply.github.com> Date: Sun, 3 May 2026 09:29:13 +0700 Subject: [PATCH 4/4] yap maaf ya, kalau judul commit aneh --- lib/screens/home/views/home_screen.dart | 4 + .../home/widgets/dashboard_charts.dart | 605 ++++++++++++++++++ 2 files changed, 609 insertions(+) create mode 100644 lib/screens/home/widgets/dashboard_charts.dart diff --git a/lib/screens/home/views/home_screen.dart b/lib/screens/home/views/home_screen.dart index 71a1af0..a495092 100644 --- a/lib/screens/home/views/home_screen.dart +++ b/lib/screens/home/views/home_screen.dart @@ -8,6 +8,7 @@ import '../../monitoring/evaporasi/blocs/evaporasi_bloc.dart'; import '../../monitoring/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart'; import '../widgets/notification_panel.dart'; import '../widgets/sensor_grid.dart'; +import '../widgets/dashboard_charts.dart'; import 'main_drawer.dart'; class HomeScreen extends StatefulWidget { @@ -199,6 +200,9 @@ class _HomeBody extends StatelessWidget { ), const SizedBox(height: 12), const SensorGrid(), + const SizedBox(height: 24), // ← TAMBAH + const DashboardCharts(), // ← TAMBAH + const SizedBox(height: 16), ], ), ); diff --git a/lib/screens/home/widgets/dashboard_charts.dart b/lib/screens/home/widgets/dashboard_charts.dart new file mode 100644 index 0000000..4dc5c45 --- /dev/null +++ b/lib/screens/home/widgets/dashboard_charts.dart @@ -0,0 +1,605 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../monitoring/wind_speed/blocs/wind_speed_bloc.dart'; +import '../../monitoring/evaporasi/blocs/evaporasi_bloc.dart'; +import '../../monitoring/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart'; + +/// Period selector khusus untuk dashboard +class DashboardPeriodSelector extends StatelessWidget { + final String selected; + final ValueChanged onChanged; + + const DashboardPeriodSelector({ + super.key, + required this.selected, + required this.onChanged, + }); + + static const _periods = ["Hari Ini", "Minggu Ini", "Bulan Ini"]; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: _periods.map((p) { + final isSelected = p == selected; + return Padding( + padding: const EdgeInsets.only(right: 8), + child: GestureDetector( + onTap: () => onChanged(p), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 7), + decoration: BoxDecoration( + color: isSelected ? Colors.blue.shade700 : Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected + ? Colors.blue.shade700 + : Colors.grey.shade300, + ), + ), + child: Text( + p, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: isSelected ? Colors.white : Colors.grey.shade600, + ), + ), + ), + ), + ); + }).toList(), + ), + ); + } +} + +/// Widget utama — semua grafik dashboard +class DashboardCharts extends StatefulWidget { + const DashboardCharts({super.key}); + + @override + State createState() => _DashboardChartsState(); +} + +class _DashboardChartsState extends State { + String _period = "Hari Ini"; + + void _onPeriodChanged(String p) { + setState(() => _period = p); + context.read().add(WindSpeedPeriodChanged(p)); + context.read().add(EvaporasiPeriodChanged(p)); + // AtmosphericBloc tidak punya period (hanya realtime) + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Header + Period Selector ────────────────────────── + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Grafik Sensor', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + ], + ), + const SizedBox(height: 10), + DashboardPeriodSelector( + selected: _period, + onChanged: _onPeriodChanged, + ), + const SizedBox(height: 14), + + // ── Scrollable chart list ───────────────────────────── + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + // 1. Kecepatan Angin + BlocBuilder( + builder: (context, state) { + final data = switch (_period) { + "Minggu Ini" => state.weeklySpeeds, + "Bulan Ini" => state.monthlySpeeds, + _ => state.dailySpeeds, + }; + return _SensorChartCard( + title: 'Kecepatan Angin', + unit: 'm/s', + color: Colors.blue.shade600, + bgColor: Colors.blue.shade50, + icon: Icons.air, + currentValue: state.currentSpeed, + data: data, + period: _period, + isLoading: state.isLoading, + ); + }, + ), + const SizedBox(width: 12), + + // 2. Evaporasi + BlocBuilder( + builder: (context, state) { + final data = switch (_period) { + "Minggu Ini" => state.dailyValues, // weekly jika ada + "Bulan Ini" => state.dailyValues, + _ => state.dailyValues, + }; + return _SensorChartCard( + title: 'Evaporasi', + unit: 'mm', + color: Colors.teal.shade600, + bgColor: Colors.teal.shade50, + icon: Icons.water_drop_outlined, + currentValue: state.currentValue, + data: data, + period: _period, + isLoading: state.isLoading, + ); + }, + ), + const SizedBox(width: 12), + + // 3. Tekanan Udara — hanya realtime (tidak ada history) + BlocBuilder( + builder: (context, state) { + return _AtmosphericCard(state: state); + }, + ), + ], + ), + ), + ], + ); + } +} + +// ══════════════════════════════════════════════════════════════════ +// Sensor Chart Card — untuk angin & evaporasi +// ══════════════════════════════════════════════════════════════════ +class _SensorChartCard extends StatelessWidget { + final String title; + final String unit; + final Color color; + final Color bgColor; + final IconData icon; + final double currentValue; + final List data; + final String period; + final bool isLoading; + + const _SensorChartCard({ + required this.title, + required this.unit, + required this.color, + required this.bgColor, + required this.icon, + required this.currentValue, + required this.data, + required this.period, + required this.isLoading, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: 260, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Container( + padding: const EdgeInsets.all(7), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: color, size: 18), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + title, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 10), + + // Nilai saat ini + if (isLoading) + Container( + height: 28, + width: 80, + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(6), + ), + ) + else + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + currentValue.toStringAsFixed(1), + style: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + color: color, + ), + ), + const SizedBox(width: 4), + Padding( + padding: const EdgeInsets.only(bottom: 3), + child: Text( + unit, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade500, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + + // Mini chart + isLoading + ? Container( + height: 70, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + ) + : _MiniLineChart( + data: data, + color: color, + period: period, + ), + + const SizedBox(height: 8), + Text( + _periodLabel(period), + style: TextStyle(fontSize: 10, color: Colors.grey.shade400), + ), + ], + ), + ); + } + + String _periodLabel(String p) { + return switch (p) { + "Minggu Ini" => "Rata-rata per hari minggu ini", + "Bulan Ini" => "Rata-rata per hari bulan ini", + _ => "Rata-rata per jam hari ini", + }; + } +} + +// ══════════════════════════════════════════════════════════════════ +// Atmospheric Card — tekanan udara (realtime only, no history) +// ══════════════════════════════════════════════════════════════════ +class _AtmosphericCard extends StatelessWidget { + final AtmosphericConditionsState state; + + const _AtmosphericCard({required this.state}); + + @override + Widget build(BuildContext context) { + // Klasifikasi tekanan udara + final (label, labelColor) = _classifyPressure(state.pressure); + + return Container( + width: 260, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Container( + padding: const EdgeInsets.all(7), + decoration: BoxDecoration( + color: Colors.purple.shade50, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(Icons.compress, + color: Colors.purple.shade400, size: 18), + ), + const SizedBox(width: 8), + const Expanded( + child: Text( + 'Kondisi Atmosfer', + style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600), + ), + ), + ], + ), + const SizedBox(height: 10), + + // Tekanan utama + if (state.isLoading) + Container( + height: 28, + width: 80, + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(6), + ), + ) + else ...[ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + state.pressure.toStringAsFixed(1), + style: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + color: Colors.purple.shade400, + ), + ), + const SizedBox(width: 4), + Padding( + padding: const EdgeInsets.only(bottom: 3), + child: Text( + 'hPa', + style: TextStyle(fontSize: 12, color: Colors.grey.shade500), + ), + ), + const Spacer(), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: labelColor.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: labelColor, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + + // Info tambahan: suhu, humidity, altitude + _AtmosInfoRow( + items: [ + _AtmosItem( + Icons.thermostat_rounded, + '${state.temperature.toStringAsFixed(1)}°C', + 'Suhu', + Colors.orange.shade400), + _AtmosItem( + Icons.water_drop_rounded, + '${state.humidity.toStringAsFixed(0)}%', + 'Kelembapan', + Colors.blue.shade400), + _AtmosItem( + Icons.landscape_rounded, + '${state.altitude.toStringAsFixed(0)} m', + 'Altitud', + Colors.green.shade400), + ], + ), + ], + + const SizedBox(height: 8), + Text( + 'Data realtime — tidak ada history', + style: TextStyle(fontSize: 10, color: Colors.grey.shade400), + ), + ], + ), + ); + } + + static (String, Color) _classifyPressure(double hpa) { + if (hpa < 1000) return ('Rendah', Colors.orange.shade600); + if (hpa > 1020) return ('Tinggi', Colors.blue.shade600); + return ('Normal', Colors.green.shade600); + } +} + +class _AtmosInfoRow extends StatelessWidget { + final List<_AtmosItem> items; + const _AtmosInfoRow({required this.items}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: items + .map((item) => Expanded( + child: Column( + children: [ + Icon(item.icon, color: item.color, size: 18), + const SizedBox(height: 3), + Text( + item.value, + style: const TextStyle( + fontSize: 12, fontWeight: FontWeight.bold), + ), + Text( + item.label, + style: + TextStyle(fontSize: 10, color: Colors.grey.shade500), + ), + ], + ), + )) + .toList(), + ); + } +} + +class _AtmosItem { + final IconData icon; + final String value; + final String label; + final Color color; + const _AtmosItem(this.icon, this.value, this.label, this.color); +} + +// ══════════════════════════════════════════════════════════════════ +// Mini Line Chart — custom painter, tanpa library tambahan +// ══════════════════════════════════════════════════════════════════ +class _MiniLineChart extends StatelessWidget { + final List data; + final Color color; + final String period; + + const _MiniLineChart({ + required this.data, + required this.color, + required this.period, + }); + + @override + Widget build(BuildContext context) { + final nonZero = data.where((v) => v > 0).toList(); + if (nonZero.isEmpty) { + return SizedBox( + height: 70, + child: Center( + child: Text( + 'Belum ada data', + style: TextStyle(fontSize: 11, color: Colors.grey.shade400), + ), + ), + ); + } + + return SizedBox( + height: 70, + child: CustomPaint( + painter: _LineChartPainter(data: data, color: color), + size: const Size(double.infinity, 70), + ), + ); + } +} + +class _LineChartPainter extends CustomPainter { + final List data; + final Color color; + + const _LineChartPainter({required this.data, required this.color}); + + @override + void paint(Canvas canvas, Size size) { + if (data.isEmpty) return; + + final maxVal = data.reduce((a, b) => a > b ? a : b); + if (maxVal == 0) return; + + final points = []; + for (int i = 0; i < data.length; i++) { + final x = i / (data.length - 1) * size.width; + final y = size.height - (data[i] / maxVal) * size.height; + points.add(Offset(x, y)); + } + + // Area fill (gradient) + final path = Path(); + path.moveTo(points.first.dx, size.height); + for (final p in points) { + path.lineTo(p.dx, p.dy); + } + path.lineTo(points.last.dx, size.height); + path.close(); + + final fillPaint = Paint() + ..shader = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [color.withValues(alpha: 0.25), color.withValues(alpha: 0.0)], + ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)); + canvas.drawPath(path, fillPaint); + + // Line + final linePaint = Paint() + ..color = color + ..strokeWidth = 2 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; + + final linePath = Path(); + linePath.moveTo(points.first.dx, points.first.dy); + for (int i = 1; i < points.length; i++) { + linePath.lineTo(points[i].dx, points[i].dy); + } + canvas.drawPath(linePath, linePaint); + + // Dot di titik terakhir + final lastPoint = points.last; + canvas.drawCircle( + lastPoint, + 3.5, + Paint()..color = color, + ); + canvas.drawCircle( + lastPoint, + 3.5, + Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5, + ); + } + + @override + bool shouldRepaint(_LineChartPainter old) => + old.data != data || old.color != color; +}