diff --git a/lib/app.dart b/lib/app.dart index 722bab2..ddd5e93 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -2,19 +2,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:klimatologiot/app_view.dart'; import 'package:user_repository/user_repository.dart'; +import 'package:monitoring_repository/monitoring_repository.dart'; import 'blocs/authentication_bloc/authentication_bloc.dart'; class MyApp extends StatelessWidget { final UserRepository userRepository; - const MyApp(this.userRepository, {super.key}); + final MonitoringRepository monitoringRepository; + const MyApp(this.userRepository, this.monitoringRepository, {super.key}); @override Widget build(BuildContext context) { return RepositoryProvider( - create: (context) => AuthenticationBloc( - userRepository: userRepository - ), - child: const MyAppView(), + create: (context) => AuthenticationBloc(userRepository: userRepository), + child: const MyAppView(), ); } -} \ No newline at end of file +} diff --git a/lib/blocs/monitoring_bloc/monitoring_bloc.dart b/lib/blocs/monitoring_bloc/monitoring_bloc.dart new file mode 100644 index 0000000..9d83ea7 --- /dev/null +++ b/lib/blocs/monitoring_bloc/monitoring_bloc.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:firebase_database/firebase_database.dart'; + +part 'monitoring_event.dart'; +part 'monitoring_state.dart'; + +class MonitoringBloc extends Bloc { + final DatabaseReference _realtimeRef = + FirebaseDatabase.instance.ref('anemometer/realtime'); + StreamSubscription? _realtimeSub; + + MonitoringBloc() : super(const MonitoringState.initial()) { + on(_onLoadMonitoringData); + on(_onMonitoringDataUpdated); + } + + void _onLoadMonitoringData( + LoadMonitoringData event, Emitter emit) { + emit(const MonitoringState.loading()); + + // Start listening to realtime data + _realtimeSub = _realtimeRef.onValue.listen((event) { + final data = event.snapshot.value; + if (data is Map) { + final speed = (data['kecepatan'] ?? 0).toDouble(); + final timestamp = data['timestamp'] ?? 0; + final waktu = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + + add(MonitoringDataUpdated(speed: speed, timestamp: waktu)); + } + }); + } + + void _onMonitoringDataUpdated( + MonitoringDataUpdated event, Emitter emit) { + emit(MonitoringState.loaded( + currentSpeed: event.speed, + lastUpdateTime: event.timestamp, + )); + } + + @override + Future close() { + _realtimeSub?.cancel(); + return super.close(); + } +} diff --git a/lib/blocs/monitoring_bloc/monitoring_event.dart b/lib/blocs/monitoring_bloc/monitoring_event.dart new file mode 100644 index 0000000..e93e753 --- /dev/null +++ b/lib/blocs/monitoring_bloc/monitoring_event.dart @@ -0,0 +1,25 @@ +part of 'monitoring_bloc.dart'; + +sealed class MonitoringEvent extends Equatable { + const MonitoringEvent(); + + @override + List get props => []; +} + +class LoadMonitoringData extends MonitoringEvent { + const LoadMonitoringData(); +} + +class MonitoringDataUpdated extends MonitoringEvent { + final double speed; + final DateTime timestamp; + + const MonitoringDataUpdated({ + required this.speed, + required this.timestamp, + }); + + @override + List get props => [speed, timestamp]; +} diff --git a/lib/blocs/monitoring_bloc/monitoring_state.dart b/lib/blocs/monitoring_bloc/monitoring_state.dart new file mode 100644 index 0000000..9e28f38 --- /dev/null +++ b/lib/blocs/monitoring_bloc/monitoring_state.dart @@ -0,0 +1,34 @@ +part of 'monitoring_bloc.dart'; + +class MonitoringState extends Equatable { + final MonitoringStatus status; + final double? currentSpeed; + final DateTime? lastUpdateTime; + + const MonitoringState._({ + this.status = MonitoringStatus.initial, + this.currentSpeed, + this.lastUpdateTime, + }); + + const MonitoringState.initial() : this._(); + + const MonitoringState.loading() : this._(status: MonitoringStatus.loading); + + const MonitoringState.loaded({ + required double currentSpeed, + required DateTime lastUpdateTime, + }) : this._( + status: MonitoringStatus.loaded, + currentSpeed: currentSpeed, + lastUpdateTime: lastUpdateTime, + ); + + const MonitoringState.error(String message) + : this._(status: MonitoringStatus.error); + + @override + List get props => [status, currentSpeed, lastUpdateTime]; +} + +enum MonitoringStatus { initial, loading, loaded, error } diff --git a/lib/main.dart b/lib/main.dart index 24a3171..eb74045 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:klimatologiot/simple_bloc_observer.dart'; import 'package:klimatologiot/app.dart'; import 'package:user_repository/user_repository.dart'; +import 'package:monitoring_repository/monitoring_repository.dart'; import 'firebase_options.dart'; void main() async { @@ -12,5 +13,8 @@ void main() async { options: DefaultFirebaseOptions.currentPlatform, ); Bloc.observer = SimpleBlocObserver(); - runApp(MyApp(FirebaseUserRepo())); -} \ No newline at end of file + runApp(MyApp( + FirebaseUserRepo(), + FirebaseMonitoringRepo(), + )); +} diff --git a/lib/screens/home/blocs/home_bloc.dart b/lib/screens/home/blocs/home_bloc.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/screens/home/blocs/home_event.dart b/lib/screens/home/blocs/home_event.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/screens/home/blocs/home_state.dart b/lib/screens/home/blocs/home_state.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/screens/home/views/home_screen.dart b/lib/screens/home/views/home_screen.dart index b933854..8521eb9 100644 --- a/lib/screens/home/views/home_screen.dart +++ b/lib/screens/home/views/home_screen.dart @@ -5,7 +5,7 @@ import 'package:firebase_database/firebase_database.dart'; import 'dart:async'; import '../../../blocs/authentication_bloc/authentication_bloc.dart'; import '../../monitoring/evaporasi_monitoring_screen.dart'; -import '../../monitoring/wind_speed/wind_speed_monitoring_screen.dart'; +import '../../monitoring/wind_speed/wind_speed.dart'; import '../../monitoring/air_quality_monitoring_screen.dart'; class HomeScreen extends StatefulWidget { diff --git a/lib/screens/monitoring/air_quality_monitoring_screen.dart b/lib/screens/monitoring/air_quality_monitoring_screen.dart index 32059d7..ea75308 100644 --- a/lib/screens/monitoring/air_quality_monitoring_screen.dart +++ b/lib/screens/monitoring/air_quality_monitoring_screen.dart @@ -1,16 +1,18 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; -import 'widgets/monitoring_shared.dart'; +import 'shared/widgets/monitoring_shared.dart'; /// AIR QUALITY MONITORING SCREEN class AirQualityMonitoringScreen extends StatefulWidget { const AirQualityMonitoringScreen({super.key}); @override - State createState() => _AirQualityMonitoringScreenState(); + State createState() => + _AirQualityMonitoringScreenState(); } -class _AirQualityMonitoringScreenState extends State { +class _AirQualityMonitoringScreenState + extends State { String _selectedPeriod = "Hari Ini"; @override @@ -61,8 +63,30 @@ class _AirQualityMonitoringScreenState extends State csvContent.writeln("Jam,AQI (Air Quality Index)"); final dailyData = [ - 68.0, 70.0, 72.0, 75.0, 78.0, 80.0, 78.0, 75.0, 72.0, 70.0, - 68.0, 65.0, 60.0, 58.0, 60.0, 62.0, 65.0, 68.0, 70.0, 72.0, 75.0, 78.0, 76.0, 74.0 + 68.0, + 70.0, + 72.0, + 75.0, + 78.0, + 80.0, + 78.0, + 75.0, + 72.0, + 70.0, + 68.0, + 65.0, + 60.0, + 58.0, + 60.0, + 62.0, + 65.0, + 68.0, + 70.0, + 72.0, + 75.0, + 78.0, + 76.0, + 74.0 ]; for (int i = 0; i < dailyData.length; i++) { csvContent.writeln("$i:00,${dailyData[i]}"); @@ -75,7 +99,8 @@ class _AirQualityMonitoringScreenState extends State csvContent.writeln("Maksimal: 80 AQI"); csvContent.writeln("Minimal: 58 AQI"); - showExportPreview(context, csvContent.toString(), "Data Harian Kualitas Udara"); + showExportPreview( + context, csvContent.toString(), "Data Harian Kualitas Udara"); } void _exportWeeklyAirQualityData() { @@ -85,7 +110,15 @@ class _AirQualityMonitoringScreenState extends State csvContent.writeln(""); csvContent.writeln("Hari,Rata-rata AQI"); - final days = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu']; + final days = [ + 'Senin', + 'Selasa', + 'Rabu', + 'Kamis', + 'Jumat', + 'Sabtu', + 'Minggu' + ]; final weeklyData = [68.0, 72.0, 75.0, 78.0, 72.0, 68.0, 62.0]; for (int i = 0; i < days.length; i++) { @@ -99,13 +132,15 @@ class _AirQualityMonitoringScreenState extends State csvContent.writeln("Maksimal: 78 AQI"); csvContent.writeln("Minimal: 62 AQI"); - showExportPreview(context, csvContent.toString(), "Data Mingguan Kualitas Udara"); + showExportPreview( + context, csvContent.toString(), "Data Mingguan Kualitas Udara"); } void _exportMonthlyAirQualityData() { final StringBuffer csvContent = StringBuffer(); csvContent.writeln("DATA MONITORING KUALITAS UDARA - BULANAN"); - csvContent.writeln("Bulan: ${DateTime.now().toString().split(' ')[0].substring(0, 7)}"); + csvContent.writeln( + "Bulan: ${DateTime.now().toString().split(' ')[0].substring(0, 7)}"); csvContent.writeln(""); csvContent.writeln("Tanggal,Rata-rata AQI"); @@ -121,7 +156,8 @@ class _AirQualityMonitoringScreenState extends State csvContent.writeln("Maksimal: 82 AQI"); csvContent.writeln("Minimal: 55 AQI"); - showExportPreview(context, csvContent.toString(), "Data Bulanan Kualitas Udara"); + showExportPreview( + context, csvContent.toString(), "Data Bulanan Kualitas Udara"); } void _exportAnnualAirQualityData() { @@ -132,11 +168,47 @@ class _AirQualityMonitoringScreenState extends State csvContent.writeln("Bulan,Rata-rata AQI,AQI Max"); final months = [ - 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', - 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember' + 'Januari', + 'Februari', + 'Maret', + 'April', + 'Mei', + 'Juni', + 'Juli', + 'Agustus', + 'September', + 'Oktober', + 'November', + 'Desember' + ]; + final averages = [ + 68.0, + 70.0, + 72.5, + 75.0, + 78.0, + 80.0, + 82.0, + 80.0, + 75.0, + 70.0, + 68.0, + 65.0 + ]; + final maxAqi = [ + 85.0, + 88.0, + 90.0, + 95.0, + 98.0, + 100.0, + 102.0, + 100.0, + 95.0, + 90.0, + 85.0, + 80.0 ]; - final averages = [68.0, 70.0, 72.5, 75.0, 78.0, 80.0, 82.0, 80.0, 75.0, 70.0, 68.0, 65.0]; - final maxAqi = [85.0, 88.0, 90.0, 95.0, 98.0, 100.0, 102.0, 100.0, 95.0, 90.0, 85.0, 80.0]; for (int i = 0; i < months.length; i++) { csvContent.writeln("${months[i]},${averages[i]},${maxAqi[i]}"); @@ -148,7 +220,8 @@ class _AirQualityMonitoringScreenState extends State csvContent.writeln("AQI Max Teringgi: 102.0"); csvContent.writeln("AQI Min Terendah: 65.0"); - showExportPreview(context, csvContent.toString(), "Data Tahunan Kualitas Udara"); + showExportPreview( + context, csvContent.toString(), "Data Tahunan Kualitas Udara"); } /// DATA GENERATORS @@ -157,8 +230,30 @@ class _AirQualityMonitoringScreenState extends State case "Hari Ini": return List.generate(24, (i) { final values = [ - 68.0, 70.0, 72.0, 75.0, 78.0, 80.0, 78.0, 75.0, 72.0, 70.0, - 68.0, 65.0, 60.0, 58.0, 60.0, 62.0, 65.0, 68.0, 70.0, 72.0, 75.0, 78.0, 76.0, 74.0 + 68.0, + 70.0, + 72.0, + 75.0, + 78.0, + 80.0, + 78.0, + 75.0, + 72.0, + 70.0, + 68.0, + 65.0, + 60.0, + 58.0, + 60.0, + 62.0, + 65.0, + 68.0, + 70.0, + 72.0, + 75.0, + 78.0, + 76.0, + 74.0 ]; return FlSpot(i.toDouble(), values[i]); }); @@ -244,7 +339,8 @@ class _AirQualityMonitoringScreenState extends State child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( - children: ["Hari Ini", "Minggu Ini", "Bulan Ini"].map((period) { + children: + ["Hari Ini", "Minggu Ini", "Bulan Ini"].map((period) { return Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip( @@ -504,8 +600,18 @@ class _AirQualityMonitoringScreenState extends State showTitles: true, getTitlesWidget: (value, meta) { final months = [ - 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', - 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des' + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'Mei', + 'Jun', + 'Jul', + 'Agu', + 'Sep', + 'Okt', + 'Nov', + 'Des' ]; return Text( months[value.toInt()], @@ -588,6 +694,7 @@ class _AirQualityMonitoringScreenState extends State color: Colors.orange.withOpacity(0.1), ), ), + /// AQI Max LineChartBarData( spots: const [ diff --git a/lib/screens/monitoring/evaporasi_monitoring_screen.dart b/lib/screens/monitoring/evaporasi_monitoring_screen.dart index 488aae8..3e67031 100644 --- a/lib/screens/monitoring/evaporasi_monitoring_screen.dart +++ b/lib/screens/monitoring/evaporasi_monitoring_screen.dart @@ -3,7 +3,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:firebase_database/firebase_database.dart'; import 'dart:async'; import 'package:intl/intl.dart'; -import 'widgets/monitoring_shared.dart'; +import 'shared/widgets/monitoring_shared.dart'; /// EVAPORASI MONITORING SCREEN class EvaporasiMonitoringScreen extends StatefulWidget { diff --git a/lib/screens/monitoring/widgets/monitoring_shared.dart b/lib/screens/monitoring/shared/widgets/monitoring_shared.dart similarity index 100% rename from lib/screens/monitoring/widgets/monitoring_shared.dart rename to lib/screens/monitoring/shared/widgets/monitoring_shared.dart diff --git a/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart new file mode 100644 index 0000000..b320ff6 --- /dev/null +++ b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart @@ -0,0 +1,15 @@ +import 'package:monitoring_repository/monitoring_repository.dart'; + +class WindSpeedBloc { + final MonitoringRepository _repo; + + WindSpeedBloc({required MonitoringRepository repo}) : _repo = repo; + + void startListening() { + // 3. Sekarang kamu bisa akses _repo di dalam lingkup class ini + _repo.getSensorStream('anemometer/realtime').listen((event) { + // Olah datanya di sini + print(event.snapshot.value); + }); + } +} diff --git a/lib/screens/monitoring/wind_speed/blocs/wind_speed_event.dart b/lib/screens/monitoring/wind_speed/blocs/wind_speed_event.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/screens/monitoring/wind_speed/blocs/wind_speed_state.dart b/lib/screens/monitoring/wind_speed/blocs/wind_speed_state.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/screens/monitoring/wind_speed/views/annual_section.dart b/lib/screens/monitoring/wind_speed/views/annual_section.dart new file mode 100644 index 0000000..b414642 --- /dev/null +++ b/lib/screens/monitoring/wind_speed/views/annual_section.dart @@ -0,0 +1,226 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; + +import '../../shared/widgets/monitoring_shared.dart'; + +class WindSpeedAnnualSection extends StatelessWidget { + final VoidCallback onExportAnnual; + + const WindSpeedAnnualSection({ + super.key, + required this.onExportAnnual, + }); + final double _rataRata = 0.0; + final double _kecepatanMax = 0.0; + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Statistik Tahunan Kecepatan Angin", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 15), + Row( + children: [ + Expanded( + child: StatCard( + title: "Rata-rata Tahunan", + value: this._rataRata.toStringAsFixed(2), + color: const Color.fromARGB(255, 76, 120, 175), + icon: Icons.air, + ), + ), + SizedBox(width: 10), + Expanded( + child: StatCard( + title: "Kecepatan Max", + value: this._kecepatanMax.toStringAsFixed(2), + color: const Color.fromARGB(255, 109, 109, 109), + icon: Icons.trending_up, + ), + ), + ], + ), + const SizedBox(height: 20), + const Text( + "Grafik Tahunan - Rata-rata & Kecepatan Maksimal", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: SizedBox( + height: 320, + child: LineChart( + LineChartData( + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + drawVerticalLine: false, + horizontalInterval: 2, + getDrawingHorizontalLine: (value) { + return FlLine( + color: const Color.fromARGB(255, 224, 224, 224), + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData( + show: true, + border: Border( + left: BorderSide( + color: Colors.grey.shade300, + width: 1, + ), + bottom: BorderSide( + color: Colors.grey.shade300, + width: 1, + ), + ), + ), + lineBarsData: [ + LineChartBarData( + spots: const [ + FlSpot(0, 5.5), + FlSpot(1, 6.2), + FlSpot(2, 6.8), + FlSpot(3, 7.5), + FlSpot(4, 7.8), + FlSpot(5, 8.0), + FlSpot(6, 8.2), + FlSpot(7, 8.0), + FlSpot(8, 7.2), + FlSpot(9, 6.5), + FlSpot(10, 5.8), + FlSpot(11, 5.2), + ], + isCurved: true, + barWidth: 2.5, + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 4, + color: Colors.green, + strokeWidth: 0, + ); + }, + ), + color: Colors.green, + belowBarData: BarAreaData( + show: true, + color: Colors.green.withOpacity(0.1), + ), + ), + LineChartBarData( + spots: const [ + FlSpot(0, 12.0), + FlSpot(1, 13.5), + FlSpot(2, 14.0), + FlSpot(3, 15.5), + FlSpot(4, 16.0), + FlSpot(5, 16.2), + FlSpot(6, 16.5), + FlSpot(7, 16.0), + FlSpot(8, 15.0), + FlSpot(9, 14.0), + FlSpot(10, 12.5), + FlSpot(11, 11.0), + ], + isCurved: true, + barWidth: 2.5, + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 4, + color: Colors.red, + strokeWidth: 0, + ); + }, + ), + color: Colors.red, + ), + ], + ), + ), + ), + ), + ), + const SizedBox(height: 15), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: Colors.green, + borderRadius: BorderRadius.circular(3), + ), + ), + const SizedBox(width: 8), + const Text( + "Rata-rata (km/h)", + style: TextStyle(fontSize: 12), + ), + const SizedBox(width: 20), + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(3), + ), + ), + const SizedBox(width: 8), + const Text( + "Kecepatan Max (km/h)", + style: TextStyle(fontSize: 12), + ), + ], + ), + const SizedBox(height: 15), + Center( + child: ElevatedButton.icon( + onPressed: onExportAnnual, + icon: const Icon(Icons.file_download), + label: const Text("Download Excel Tahunan"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ), + ), + ], + ); + } +} diff --git a/lib/screens/monitoring/wind_speed/widgets/wind_speed_explanation.dart b/lib/screens/monitoring/wind_speed/views/explanation.dart similarity index 70% rename from lib/screens/monitoring/wind_speed/widgets/wind_speed_explanation.dart rename to lib/screens/monitoring/wind_speed/views/explanation.dart index 6e6bb90..49b3340 100644 --- a/lib/screens/monitoring/wind_speed/widgets/wind_speed_explanation.dart +++ b/lib/screens/monitoring/wind_speed/views/explanation.dart @@ -8,10 +8,16 @@ class WindSpeedExplanation extends StatelessWidget { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.green.shade50, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.green.shade200), - ), + color: const Color.fromARGB(255, 232, 240, 245), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 3, + spreadRadius: 0, + offset: const Offset(0, 5), + ) + ]), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: const [ @@ -20,7 +26,7 @@ class WindSpeedExplanation extends StatelessWidget { style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, - color: Colors.green, + color: Color.fromARGB(255, 0, 0, 0), ), ), SizedBox(height: 10), @@ -29,7 +35,7 @@ class WindSpeedExplanation extends StatelessWidget { style: TextStyle( fontSize: 12, height: 1.6, - color: Colors.green, + color: Color.fromARGB(255, 0, 0, 0), ), ), ], diff --git a/lib/screens/monitoring/wind_speed/views/graph_block.dart b/lib/screens/monitoring/wind_speed/views/graph_block.dart new file mode 100644 index 0000000..c24a730 --- /dev/null +++ b/lib/screens/monitoring/wind_speed/views/graph_block.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; + +import '../../shared/widgets/monitoring_shared.dart'; + +class WindSpeedGraphBlock extends StatelessWidget { + final String selectedPeriod; + final List dailySpeeds; + final bool isOnline; + final ValueChanged onPeriodChanged; + final ValueChanged onExportPressed; + + const WindSpeedGraphBlock({ + super.key, + required this.selectedPeriod, + required this.dailySpeeds, + required this.isOnline, + required this.onPeriodChanged, + required this.onExportPressed, + }); + + List _getPeriodSpots() { + switch (selectedPeriod) { + case "Hari Ini": + return List.generate(24, (i) { + final value = dailySpeeds[i]; + return FlSpot(i.toDouble(), value); + }); + case "Minggu Ini": + return const [ + FlSpot(0, 6.5), + FlSpot(1, 7.2), + FlSpot(2, 7.8), + FlSpot(3, 8.0), + FlSpot(4, 7.5), + FlSpot(5, 6.8), + FlSpot(6, 5.5), + ]; + case "Bulan Ini": + return const [ + FlSpot(0, 6.0), + FlSpot(1, 6.5), + FlSpot(2, 7.0), + FlSpot(3, 7.5), + FlSpot(4, 7.8), + FlSpot(5, 8.0), + FlSpot(6, 8.2), + FlSpot(7, 8.0), + FlSpot(8, 7.5), + FlSpot(9, 7.0), + FlSpot(10, 6.5), + FlSpot(11, 6.0), + ]; + default: + return []; + } + } + + @override + Widget build(BuildContext context) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Status Kecepatan Angin", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + decoration: BoxDecoration( + color: + isOnline ? Colors.green.shade100 : Colors.red.shade100, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + isOnline ? "ONLINE" : "OFFLINE", + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: isOnline ? Colors.green : Colors.red, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: + ["Hari Ini", "Minggu Ini", "Bulan Ini"].map((period) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: ChoiceChip( + label: Text(period), + selected: selectedPeriod == period, + onSelected: (selected) { + if (selected) { + onPeriodChanged(period); + } + }, + side: BorderSide.none, + backgroundColor: Colors.grey.withOpacity(0.1), + selectedColor: + const Color.fromARGB(255, 0, 136, 255), + labelStyle: TextStyle( + color: selectedPeriod == period + ? Colors.white + : Colors.black, + ), + ), + ); + }).toList(), + ), + ), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () => onExportPressed(selectedPeriod), + icon: const Icon(Icons.download, size: 16), + label: const Text("Export"), + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 0, 145, 255), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + textStyle: const TextStyle(fontSize: 11), + ), + ), + ], + ), + const SizedBox(height: 16), + SizedBox( + height: 300, + child: LineChart( + LineChartData( + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + drawVerticalLine: false, + horizontalInterval: 1, + getDrawingHorizontalLine: (value) { + return FlLine( + color: Colors.grey.shade300, + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: getPeriodInterval(selectedPeriod), + getTitlesWidget: (value, meta) { + return Text( + getPeriodLabel(selectedPeriod, value), + style: const TextStyle( + fontSize: 10, + color: Colors.grey, + ), + ); + }, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: 1, + getTitlesWidget: (value, meta) { + return Text( + value.toInt().toString(), + style: const TextStyle( + fontSize: 9, + color: Colors.grey, + ), + ); + }, + reservedSize: 28, + ), + ), + topTitles: + AxisTitles(sideTitles: SideTitles(showTitles: false)), + rightTitles: + AxisTitles(sideTitles: SideTitles(showTitles: false)), + ), + borderData: FlBorderData( + show: true, + border: Border( + left: BorderSide( + color: Colors.grey.shade300, + width: 1, + ), + bottom: BorderSide( + color: Colors.grey.shade300, + width: 1, + ), + ), + ), + lineBarsData: [ + LineChartBarData( + spots: _getPeriodSpots(), + isCurved: true, + barWidth: 2.5, + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 4, + color: const Color.fromARGB(255, 0, 191, 255), + strokeWidth: 0, + ); + }, + ), + color: const Color.fromARGB(255, 0, 191, 255), + belowBarData: BarAreaData( + show: true, + color: const Color.fromARGB(255, 0, 115, 255) + .withOpacity(0.1), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/monitoring/wind_speed/views/status_block.dart b/lib/screens/monitoring/wind_speed/views/status_block.dart new file mode 100644 index 0000000..24b305a --- /dev/null +++ b/lib/screens/monitoring/wind_speed/views/status_block.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class WindSpeedStatusBlock extends StatelessWidget { + final double currentSpeed; + final bool isOnline; + final DateTime lastUpdateTime; + + const WindSpeedStatusBlock({ + super.key, + required this.currentSpeed, + required this.isOnline, + required this.lastUpdateTime, + }); + + @override + Widget build(BuildContext context) { + final formattedTime = + DateFormat("dd MMM yyyy • HH:mm:ss").format(lastUpdateTime); + + /// ====== Current Speed bagian kode kecepatan angin ===== /// + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.12), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Kecepatan Angin", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${currentSpeed.toStringAsFixed(2)} km/h", + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Color.fromARGB(255, 0, 0, 0), + ), + ), + + /// ==== bagian online offline status tampilan ==== /// + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + decoration: BoxDecoration( + color: + isOnline ? Colors.green.shade100 : Colors.red.shade100, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + isOnline ? "ONLINE" : "OFFLINE", + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: isOnline ? Colors.green : Colors.red, + ), + ), + ), + ], + ), + const SizedBox(height: 10), + Text( + "Update: $formattedTime", + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/monitoring/wind_speed/widgets/wind_speed_annual_section.dart b/lib/screens/monitoring/wind_speed/views/wind_speed_annual_section.dart similarity index 96% rename from lib/screens/monitoring/wind_speed/widgets/wind_speed_annual_section.dart rename to lib/screens/monitoring/wind_speed/views/wind_speed_annual_section.dart index 60806c7..b414642 100644 --- a/lib/screens/monitoring/wind_speed/widgets/wind_speed_annual_section.dart +++ b/lib/screens/monitoring/wind_speed/views/wind_speed_annual_section.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; -import '../../widgets/monitoring_shared.dart'; +import '../../shared/widgets/monitoring_shared.dart'; class WindSpeedAnnualSection extends StatelessWidget { final VoidCallback onExportAnnual; @@ -31,7 +31,7 @@ class WindSpeedAnnualSection extends StatelessWidget { child: StatCard( title: "Rata-rata Tahunan", value: this._rataRata.toStringAsFixed(2), - color: Colors.green, + color: const Color.fromARGB(255, 76, 120, 175), icon: Icons.air, ), ), @@ -40,7 +40,7 @@ class WindSpeedAnnualSection extends StatelessWidget { child: StatCard( title: "Kecepatan Max", value: this._kecepatanMax.toStringAsFixed(2), - color: Colors.red, + color: const Color.fromARGB(255, 109, 109, 109), icon: Icons.trending_up, ), ), @@ -73,7 +73,7 @@ class WindSpeedAnnualSection extends StatelessWidget { horizontalInterval: 2, getDrawingHorizontalLine: (value) { return FlLine( - color: Colors.grey.shade300, + color: const Color.fromARGB(255, 224, 224, 224), strokeWidth: 1, ); }, @@ -142,7 +142,7 @@ class WindSpeedAnnualSection extends StatelessWidget { ), LineChartBarData( spots: const [ - FlSpot(0, 12.0), //sadds + FlSpot(0, 12.0), FlSpot(1, 13.5), FlSpot(2, 14.0), FlSpot(3, 15.5), diff --git a/lib/screens/monitoring/wind_speed/widgets/wind_speed_graph_block.dart b/lib/screens/monitoring/wind_speed/views/wind_speed_graph_block.dart similarity index 92% rename from lib/screens/monitoring/wind_speed/widgets/wind_speed_graph_block.dart rename to lib/screens/monitoring/wind_speed/views/wind_speed_graph_block.dart index 5a52f97..c24a730 100644 --- a/lib/screens/monitoring/wind_speed/widgets/wind_speed_graph_block.dart +++ b/lib/screens/monitoring/wind_speed/views/wind_speed_graph_block.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; -import '../../widgets/monitoring_shared.dart'; +import '../../shared/widgets/monitoring_shared.dart'; class WindSpeedGraphBlock extends StatelessWidget { final String selectedPeriod; @@ -119,7 +119,10 @@ class WindSpeedGraphBlock extends StatelessWidget { onPeriodChanged(period); } }, - selectedColor: Colors.green, + side: BorderSide.none, + backgroundColor: Colors.grey.withOpacity(0.1), + selectedColor: + const Color.fromARGB(255, 0, 136, 255), labelStyle: TextStyle( color: selectedPeriod == period ? Colors.white @@ -137,7 +140,7 @@ class WindSpeedGraphBlock extends StatelessWidget { icon: const Icon(Icons.download, size: 16), label: const Text("Export"), style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange, + backgroundColor: const Color.fromARGB(255, 0, 145, 255), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 10, @@ -226,15 +229,16 @@ class WindSpeedGraphBlock extends StatelessWidget { getDotPainter: (spot, percent, barData, index) { return FlDotCirclePainter( radius: 4, - color: Colors.green, + color: const Color.fromARGB(255, 0, 191, 255), strokeWidth: 0, ); }, ), - color: Colors.green, + color: const Color.fromARGB(255, 0, 191, 255), belowBarData: BarAreaData( show: true, - color: Colors.green.withOpacity(0.1), + color: const Color.fromARGB(255, 0, 115, 255) + .withOpacity(0.1), ), ), ], diff --git a/lib/screens/monitoring/wind_speed/widgets/wind_speed_status_block.dart b/lib/screens/monitoring/wind_speed/widgets/wind_speed_status_block.dart deleted file mode 100644 index d3e5177..0000000 --- a/lib/screens/monitoring/wind_speed/widgets/wind_speed_status_block.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -class WindSpeedStatusBlock extends StatelessWidget { - final double currentSpeed; - final bool isOnline; - final DateTime lastUpdateTime; - - const WindSpeedStatusBlock({ - super.key, - required this.currentSpeed, - required this.isOnline, - required this.lastUpdateTime, - }); - - @override - Widget build(BuildContext context) { - final formattedTime = - DateFormat("dd MMM yyyy • HH:mm:ss").format(lastUpdateTime); - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${currentSpeed.toStringAsFixed(2)} km/h", - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.green, - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 5, - ), - decoration: BoxDecoration( - color: isOnline ? Colors.green.shade100 : Colors.red.shade100, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - isOnline ? "ONLINE" : "OFFLINE", - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: isOnline ? Colors.green : Colors.red, - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - "Update: $formattedTime", - style: const TextStyle( - fontSize: 12, - color: Colors.grey, - ), - ), - ], - ), - ); - } -} diff --git a/lib/screens/monitoring/wind_speed/wind_speed.dart b/lib/screens/monitoring/wind_speed/wind_speed.dart new file mode 100644 index 0000000..db2c12a --- /dev/null +++ b/lib/screens/monitoring/wind_speed/wind_speed.dart @@ -0,0 +1,272 @@ +import 'dart:async'; + +import '../'; +import 'package:flutter/material.dart'; + +import '../shared/widgets/monitoring_shared.dart'; +import 'views/annual_section.dart'; +import 'views/explanation.dart'; +import 'views/graph_block.dart'; +import 'views/status_block.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class WindSpeedMonitoringScreen extends StatefulWidget { + const WindSpeedMonitoringScreen({super.key}); + + @override + State createState() => + _WindSpeedMonitoringScreenState(); +} + +class _WindSpeedMonitoringScreenState extends State { + String _selectedPeriod = "Hari Ini"; + + // realtime values + DateTime? _lastUpdateTime; + double _currentSpeed = 0.0; + final List _dailySpeeds = List.filled(24, 0.0); + + // hourly aggregation helpers (PRO version) + double _hourlyTotalSpeed = 0.0; + int _hourlyCount = 0; + int _currentHour = DateTime.now().hour; + + bool get _isOnline { + if (_lastUpdateTime == null) return false; + final diff = DateTime.now().difference(_lastUpdateTime!); + return diff.inSeconds <= 5; + } + + @override + void initState() { + super.initState(); + _realtimeSub = _realtimeRef.onValue.listen(_onRealtimeData); + } + + void _onRealtimeData(DatabaseEvent event) { + final data = event.snapshot.value; + + if (data is Map) { + final speed = (data['kecepatan'] ?? 0).toDouble(); + final timestamp = data['timestamp'] ?? 0; + + final waktu = + DateTime.fromMillisecondsSinceEpoch(timestamp * 1000).toLocal(); + + setState(() { + _currentSpeed = speed; + _lastUpdateTime = waktu; + + final incomingHour = waktu.hour; + + if (incomingHour != _currentHour) { + if (_hourlyCount > 0) { + final avg = _hourlyTotalSpeed / _hourlyCount; + _dailySpeeds[_currentHour] = avg; + } + + _hourlyTotalSpeed = 0.0; + _hourlyCount = 0; + _currentHour = incomingHour; + + if (incomingHour == 0) { + for (int i = 0; i < 24; i++) { + _dailySpeeds[i] = 0.0; + } + } + } + + _hourlyTotalSpeed += speed; + _hourlyCount++; + }); + } + } + + @override + void dispose() { + _realtimeSub?.cancel(); + super.dispose(); + } + + void _handleExportSelectedPeriod(String period) { + switch (period) { + case "Hari Ini": + _exportDailyWindData(); + break; + case "Minggu Ini": + _exportWeeklyWindData(); + break; + case "Bulan Ini": + _exportMonthlyWindData(); + break; + } + } + + void _exportDailyWindData() { + final StringBuffer csvContent = StringBuffer(); + csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - HARIAN (24 JAM)"); + csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}"); + csvContent.writeln(""); + csvContent.writeln("Jam,Kecepatan (km/h)"); + + final dailyData = []; + + for (int i = 0; i < dailyData.length; i++) { + csvContent.writeln("$i:00,${dailyData[i]}"); + } + + csvContent.writeln(""); + csvContent.writeln("Statistik Harian"); + csvContent.writeln("Total: 165.8 km/h"); + csvContent.writeln("Rata-rata: 6.9 km/h"); + csvContent.writeln("Maksimal: 11.0 km/h"); + csvContent.writeln("Minimal: 4.3 km/h"); + + showExportPreview( + context, csvContent.toString(), "Data Harian Kecepatan Angin"); + } + + void _exportWeeklyWindData() { + final StringBuffer csvContent = StringBuffer(); + csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - MINGGUAN"); + csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}"); + csvContent.writeln(""); + csvContent.writeln("Hari,Rata-rata (km/h)"); + + final days = [ + 'Senin', + 'Selasa', + 'Rabu', + 'Kamis', + 'Jumat', + 'Sabtu', + 'Minggu' + ]; + final weeklyData = [6.5, 7.2, 7.8, 8.0, 7.5, 6.8, 5.5]; + + for (int i = 0; i < days.length; i++) { + csvContent.writeln("${days[i]},${weeklyData[i]}"); + } + + csvContent.writeln(""); + csvContent.writeln("Statistik Mingguan"); + csvContent.writeln("Total: 49.3 km/h"); + csvContent.writeln("Rata-rata: 7.0 km/h"); + csvContent.writeln("Maksimal: 8.0 km/h"); + csvContent.writeln("Minimal: 5.5 km/h"); + + showExportPreview( + context, csvContent.toString(), "Data Mingguan Kecepatan Angin"); + } + + void _exportMonthlyWindData() { + final StringBuffer csvContent = StringBuffer(); + csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - BULANAN"); + csvContent.writeln( + "Bulan: ${DateTime.now().toString().split(' ')[0].substring(0, 7)}"); + csvContent.writeln(""); + csvContent.writeln("Tanggal,Rata-rata (km/h)"); + + for (int i = 1; i <= 28; i++) { + final value = (6.0 + (i % 6) * 0.8).toStringAsFixed(1); + csvContent.writeln("$i,$value"); + } + + csvContent.writeln(""); + csvContent.writeln("Statistik Bulanan"); + csvContent.writeln("Total: 180.5 km/h"); + csvContent.writeln("Rata-rata: 6.4 km/h"); + csvContent.writeln("Maksimal: 9.0 km/h"); + csvContent.writeln("Minimal: 4.0 km/h"); + + showExportPreview( + context, csvContent.toString(), "Data Bulanan Kecepatan Angin"); + } + + void _exportAnnualWindData() { + final StringBuffer csvContent = StringBuffer(); + csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - TAHUNAN"); + csvContent.writeln("Tahun: 2025"); + csvContent.writeln(""); + csvContent.writeln("Bulan,Rata-rata (km/h),Kecepatan Max (km/h)"); + + final months = [ + 'Januari', + 'Februari', + 'Maret', + 'April', + 'Mei', + 'Juni', + 'Juli', + 'Agustus', + 'September', + 'Oktober', + 'November', + 'Desember' + ]; + final averages = []; + final maxSpeed = []; + + for (int i = 0; i < months.length; i++) { + csvContent.writeln("${months[i]},${averages[i]},${maxSpeed[i]}"); + } + + csvContent.writeln(""); + csvContent.writeln("STATISTIK TAHUNAN"); + csvContent.writeln("Rata-rata Tahunan: 7.1 km/h"); + csvContent.writeln("Kecepatan Max Tertinggi: 16.5 km/h"); + csvContent.writeln("Kecepatan Min Terendah: 5.2 km/h"); + + showExportPreview( + context, csvContent.toString(), "Data Tahunan Kecepatan Angin"); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + "Wind Speed", + style: GoogleFonts.rubik( + color: Colors.black, fontWeight: FontWeight.bold), + ), + centerTitle: true, + backgroundColor: const Color.fromARGB(37, 158, 158, 158), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_lastUpdateTime != null) + WindSpeedStatusBlock( + currentSpeed: _currentSpeed, + isOnline: _isOnline, + lastUpdateTime: _lastUpdateTime!, + ), + const SizedBox(height: 20), + WindSpeedGraphBlock( + selectedPeriod: _selectedPeriod, + dailySpeeds: _dailySpeeds, + isOnline: _isOnline, + onPeriodChanged: (period) { + setState(() { + _selectedPeriod = period; + }); + }, + onExportPressed: _handleExportSelectedPeriod, + ), + const SizedBox(height: 25), + const WindSpeedExplanation(), + const SizedBox(height: 25), + WindSpeedAnnualSection( + onExportAnnual: _exportAnnualWindData, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/monitoring/wind_speed/wind_speed_monitoring_screen.dart b/lib/screens/monitoring/wind_speed/wind_speed_monitoring_screen.dart index cbddc47..b60ca89 100644 --- a/lib/screens/monitoring/wind_speed/wind_speed_monitoring_screen.dart +++ b/lib/screens/monitoring/wind_speed/wind_speed_monitoring_screen.dart @@ -3,11 +3,11 @@ import 'dart:async'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/material.dart'; -import '../widgets/monitoring_shared.dart'; -import 'widgets/wind_speed_annual_section.dart'; -import 'widgets/wind_speed_explanation.dart'; -import 'widgets/wind_speed_graph_block.dart'; -import 'widgets/wind_speed_status_block.dart'; +import '../shared/widgets/monitoring_shared.dart'; +import 'views/annual_section.dart'; +import 'views/explanation.dart'; +import 'views/graph_block.dart'; +import 'views/status_block.dart'; import 'package:google_fonts/google_fonts.dart'; class WindSpeedMonitoringScreen extends StatefulWidget { @@ -21,21 +21,6 @@ class WindSpeedMonitoringScreen extends StatefulWidget { class _WindSpeedMonitoringScreenState extends State { String _selectedPeriod = "Hari Ini"; - // realtime database state - final DatabaseReference _realtimeRef = - FirebaseDatabase.instance.ref('anemometer/realtime'); - StreamSubscription? _realtimeSub; - - // realtime values - DateTime? _lastUpdateTime; - double _currentSpeed = 0.0; - final List _dailySpeeds = List.filled(24, 0.0); - - // hourly aggregation helpers (PRO version) - double _hourlyTotalSpeed = 0.0; - int _hourlyCount = 0; - int _currentHour = DateTime.now().hour; - bool get _isOnline { if (_lastUpdateTime == null) return false; final diff = DateTime.now().difference(_lastUpdateTime!); @@ -55,7 +40,8 @@ class _WindSpeedMonitoringScreenState extends State { final speed = (data['kecepatan'] ?? 0).toDouble(); final timestamp = data['timestamp'] ?? 0; - final waktu = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + final waktu = + DateTime.fromMillisecondsSinceEpoch(timestamp * 1000).toLocal(); setState(() { _currentSpeed = speed; @@ -113,32 +99,7 @@ class _WindSpeedMonitoringScreenState extends State { csvContent.writeln(""); csvContent.writeln("Jam,Kecepatan (km/h)"); - final dailyData = [ - 5.0, - 5.2, - 4.8, - 4.5, - 4.3, - 4.6, - 5.1, - 6.0, - 7.2, - 8.5, - 9.0, - 10.2, - 11.0, - 10.5, - 10.0, - 9.2, - 8.0, - 6.5, - 5.5, - 5.0, - 4.8, - 4.5, - 5.0, - 5.2 - ]; + final dailyData = []; for (int i = 0; i < dailyData.length; i++) { csvContent.writeln("$i:00,${dailyData[i]}"); @@ -233,34 +194,8 @@ class _WindSpeedMonitoringScreenState extends State { 'November', 'Desember' ]; - final averages = [ - 5.5, - 6.2, - 6.8, - 7.5, - 7.8, - 8.0, - 8.2, - 8.0, - 7.2, - 6.5, - 5.8, - 5.2 - ]; - final maxSpeed = [ - 12.0, - 13.5, - 14.0, - 15.5, - 16.0, - 16.2, - 16.5, - 16.0, - 15.0, - 14.0, - 12.5, - 11.0 - ]; + final averages = []; + final maxSpeed = []; for (int i = 0; i < months.length; i++) { csvContent.writeln("${months[i]},${averages[i]},${maxSpeed[i]}"); @@ -294,20 +229,13 @@ class _WindSpeedMonitoringScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - "Kecepatan Angin", - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 20), if (_lastUpdateTime != null) WindSpeedStatusBlock( currentSpeed: _currentSpeed, isOnline: _isOnline, lastUpdateTime: _lastUpdateTime!, ), + const SizedBox(height: 20), WindSpeedGraphBlock( selectedPeriod: _selectedPeriod, dailySpeeds: _dailySpeeds, diff --git a/packages/monitoring_repository/lib/monitoring_repository.dart b/packages/monitoring_repository/lib/monitoring_repository.dart new file mode 100644 index 0000000..626b971 --- /dev/null +++ b/packages/monitoring_repository/lib/monitoring_repository.dart @@ -0,0 +1,4 @@ +library monitoring_repository; + +export 'src/monitoring_repo.dart'; +export 'src/firebase_monitoring_repo.dart'; diff --git a/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart b/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart new file mode 100644 index 0000000..465237d --- /dev/null +++ b/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart @@ -0,0 +1,23 @@ +import 'dart:developer'; +import 'dart:async'; +import 'package:rxdart/rxdart.dart'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:monitoring_repository/monitoring_repository.dart'; + +/// === ambil data dari realtime database firebase === /// +class FirebaseMonitoringRepo implements MonitoringRepository { + final FirebaseDatabase _db = FirebaseDatabase.instance; + + // Satu fungsi untuk semua jenis sensor + // Kamu cukup masukkan "path" database-nya saja + @override + Stream getSensorStream(String path) { + return _db.ref(path).onValue; + } + + @override + // Jika ingin mengambil data sekali saja (bukan stream) + Future getSensorSnapshot(String path) async { + return await _db.ref(path).get(); + } +} diff --git a/packages/monitoring_repository/lib/src/models/models.dart b/packages/monitoring_repository/lib/src/models/models.dart new file mode 100644 index 0000000..fd4175e --- /dev/null +++ b/packages/monitoring_repository/lib/src/models/models.dart @@ -0,0 +1 @@ +export 'wind_speed.dart'; diff --git a/packages/monitoring_repository/lib/src/models/wind_speed.dart b/packages/monitoring_repository/lib/src/models/wind_speed.dart new file mode 100644 index 0000000..221cfb6 --- /dev/null +++ b/packages/monitoring_repository/lib/src/models/wind_speed.dart @@ -0,0 +1,21 @@ +class MyWindSpeed { + double speed; + int pulse; + DateTime timestamp; + + MyWindSpeed({ + required this.speed, + required this.pulse, + required this.timestamp, + }); + + factory MyWindSpeed.fromJson(Map json) { + return MyWindSpeed( + speed: (json['speed'] ?? 0).toDouble(), + pulse: (json['pulse'] ?? 0) as int, + timestamp: DateTime.fromMillisecondsSinceEpoch( + (json['timestamp'] ?? 0) * 1000, // kalau dari Unix detik + ), + ); + } +} diff --git a/packages/monitoring_repository/lib/src/monitoring_repo.dart b/packages/monitoring_repository/lib/src/monitoring_repo.dart new file mode 100644 index 0000000..83b7bba --- /dev/null +++ b/packages/monitoring_repository/lib/src/monitoring_repo.dart @@ -0,0 +1,7 @@ +import 'package:firebase_database/firebase_database.dart'; + +abstract class MonitoringRepository { + Stream getSensorStream(String path); + + Future getSensorSnapshot(String path); +} diff --git a/packages/monitoring_repository/pubspec.lock b/packages/monitoring_repository/pubspec.lock new file mode 100644 index 0000000..db981f1 --- /dev/null +++ b/packages/monitoring_repository/pubspec.lock @@ -0,0 +1,290 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: f698de6eb8a0dd7a9a931bbfe13568e8b77e702eb2deb13dd83480c5373e7746 + url: "https://pub.dev" + source: hosted + version: "1.3.68" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + firebase_core: + dependency: transitive + description: + name: firebase_core + sha256: "2f988dab915efde3b3105268dbd69efce0e8570d767a218ccd914afd0c10c8cc" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "1399ab1f0ac3b503d8a9be64a4c997fc066bbf33f701f42866e5569f26205ebe" + url: "https://pub.dev" + source: hosted + version: "3.5.1" + firebase_database: + dependency: "direct main" + description: + name: firebase_database + sha256: "05358855166c943a2fe9321bce69ab6b583c9a49e70e01b5297ef03f8bea50b9" + url: "https://pub.dev" + source: hosted + version: "12.2.0" + firebase_database_platform_interface: + dependency: transitive + description: + name: firebase_database_platform_interface + sha256: "83278cc315ad0f36a5e30fc7f3d1c6edc722340347d41ea17f57e52176bb98f8" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + firebase_database_web: + dependency: transitive + description: + name: firebase_database_web + sha256: "77df7f7c59f4bba33f06644f76e28a8acc594487f1aaa4c79947abf491a7ae70" + url: "https://pub.dev" + source: hosted + version: "0.2.7+5" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + rxdart: + dependency: "direct main" + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" +sdks: + dart: ">=3.10.7 <4.0.0" + flutter: ">=3.22.0" diff --git a/packages/monitoring_repository/pubspec.yaml b/packages/monitoring_repository/pubspec.yaml new file mode 100644 index 0000000..c58f077 --- /dev/null +++ b/packages/monitoring_repository/pubspec.yaml @@ -0,0 +1,56 @@ +name: monitoring_repository +description: 'A new Flutter package project.' +version: 0.0.1 +homepage: + +environment: + sdk: ^3.10.7 + flutter: '>=1.17.0' + +dependencies: + flutter: + sdk: flutter + rxdart: ^0.27.7 + firebase_database: ^12.1.3 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/pubspec.lock b/pubspec.lock index db41fa9..a88d640 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -432,6 +432,13 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + monitoring_repository: + dependency: "direct main" + description: + path: "packages/monitoring_repository" + relative: true + source: path + version: "0.0.1" native_toolchain_c: dependency: transitive description: @@ -669,5 +676,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.3 <4.0.0" + dart: ">=3.10.7 <4.0.0" flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index 179a7fd..a9c35a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: klimatologiot -description: "A new Flutter project." +description: 'A new Flutter project.' publish_to: 'none' version: 1.0.0+1 environment: @@ -22,6 +22,8 @@ dependencies: rxdart: ^0.27.7 user_repository: path: packages/user_repository + monitoring_repository: + path: packages/monitoring_repository dev_dependencies: flutter_lints: ^3.0.0 diff --git a/test/widget_test.dart b/test/widget_test.dart index 7096fb9..7a974a8 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,15 +5,80 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. +import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:monitoring_repository/monitoring_repository.dart'; +import 'package:user_repository/user_repository.dart'; +import '../lib/app.dart'; -import 'package:klimatologiot/main.dart'; +// import 'package:klimatologiot/main.dart'; + +class FakeUserRepository implements UserRepository { + @override + Stream get user => const Stream.empty(); + + @override + Future logOut() async {} + + @override + Future setUserData(MyUser user) { + // TODO: implement setUserData + throw UnimplementedError(); + } + + @override + Future signIn(String email, String password) { + // TODO: implement signIn + throw UnimplementedError(); + } + + @override + Future signInWithGoogle() { + // TODO: implement signInWithGoogle + throw UnimplementedError(); + } + + @override + Future signUp(MyUser myUser, String password) { + // TODO: implement signUp + throw UnimplementedError(); + } + + // tambahin kalau ada method lain +} + +class FakeMonitoringRepository implements MonitoringRepository { + get _db => null; + + // Satu fungsi untuk semua jenis sensor + // Kamu cukup masukkan "path" database-nya saja + + @override + // Jika ingin mengambil data sekali saja (bukan stream) + Future getSensorSnapshot(String path) async { + return await _db.ref(path).get(); + } + + @override + Stream getSensorStream(String path) { + // TODO: implement getSensorStream + throw UnimplementedError(); + } +} void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { + testWidgets('App loads', (WidgetTester tester) async { + final fakeRepo = FakeUserRepository(); + final fakeMonitoring = FakeUserRepository(); // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget( + MyApp(fakeRepo, fakeMonitoring as MonitoringRepository), + ); + + await tester.pump(); + + expect(find.byType(MaterialApp), findsOneWidget); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget);