From 9ec9beda7bfbbcd667b9032e6ebd19a8610c8b49 Mon Sep 17 00:00:00 2001 From: Mochamad ongki ramadani Date: Thu, 12 Feb 2026 22:59:01 +0700 Subject: [PATCH] sistem pengukuran evaporasi home page --- lib/app_view.dart | 7 +- lib/screens/home/home_screen.dart | 474 +++++++++++ lib/screens/home/views/home_screen.dart | 12 - .../air_quality_monitoring_screen.dart | 677 ++++++++++++++++ .../evaporasi_monitoring_screen.dart | 741 ++++++++++++++++++ .../monitoring/widgets/monitoring_shared.dart | 130 +++ .../wind_speed_monitoring_screen.dart | 677 ++++++++++++++++ packages/user_repository/pubspec.lock | 62 +- pubspec.lock | 8 + pubspec.yaml | 5 + 10 files changed, 2750 insertions(+), 43 deletions(-) create mode 100644 lib/screens/home/home_screen.dart delete mode 100644 lib/screens/home/views/home_screen.dart create mode 100644 lib/screens/monitoring/air_quality_monitoring_screen.dart create mode 100644 lib/screens/monitoring/evaporasi_monitoring_screen.dart create mode 100644 lib/screens/monitoring/widgets/monitoring_shared.dart create mode 100644 lib/screens/monitoring/wind_speed_monitoring_screen.dart diff --git a/lib/app_view.dart b/lib/app_view.dart index 3fd5d03..60dc938 100644 --- a/lib/app_view.dart +++ b/lib/app_view.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:klimatologiot/blocs/authentication_bloc/authentication_bloc.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import 'package:klimatologiot/blocs/authentication_bloc/authentication_bloc.dart'; -import 'package:klimatologiot/screens/auth/views/welcome_screen.dart'; -import 'package:klimatologiot/screens/home/views/home_screen.dart'; +import 'package:klimatologiot/screens/home/home_screen.dart'; class MyAppView extends StatelessWidget { const MyAppView({super.key}); diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart new file mode 100644 index 0000000..4bb2f06 --- /dev/null +++ b/lib/screens/home/home_screen.dart @@ -0,0 +1,474 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import '../monitoring/evaporasi_monitoring_screen.dart'; +import '../monitoring/wind_speed_monitoring_screen.dart'; +import '../monitoring/air_quality_monitoring_screen.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Monitoring Klimatologi"), + centerTitle: true, + ), + + /// 🔹 SIDEBAR (MENU 3 GARIS DI KIRI) + drawer: Drawer( + child: SafeArea( + child: ListView( + padding: EdgeInsets.zero, + children: [ + // 🔹 HEADER DENGAN AKUN INFO + UserAccountsDrawerHeader( + accountName: const Text( + "Admin Klimatologi", + style: TextStyle(fontWeight: FontWeight.bold), + ), + accountEmail: const Text("admin@klimatologi.com"), + currentAccountPicture: CircleAvatar( + backgroundColor: Colors.blue, + child: const Text( + "A", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + + // 🔹 DASHBOARD MENU + ListTile( + leading: const Icon(Icons.dashboard), + title: const Text("Dashboard"), + onTap: () { + Navigator.pop(context); + }, + ), + + const Divider(), + + // 🔹 MONITORING MENU + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + "Monitoring", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + ), + + _buildDrawerItem( + context, + "Evaporasi", + Icons.opacity, + ), + _buildDrawerItem( + context, + "Kecepatan Angin", + Icons.air, + ), + _buildDrawerItem( + context, + "Kualitas Udara", + Icons.cloud, + ), + ], + ), + ), + ), + + /// 🔹 DASHBOARD UTAMA - 3 MONITORING + body: Padding( + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Selamat Datang Admin Klimatologi", + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), + + // 🔹 3 BLOCK DATA HARI INI + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildDataBlock( + "Evaporasi", + "45.2", + "mm", + Colors.blue, + Icons.opacity, + ), + _buildDataBlock( + "Kec. Angin", + "12.5", + "km/h", + Colors.green, + Icons.air, + ), + _buildDataBlock( + "Kualitas Udara", + "78", + "AQI", + Colors.orange, + Icons.cloud, + ), + ], + ), + + const SizedBox(height: 30), + + const Text( + "Grafik Monitoring", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 15), + + _buildDashboardCard(context, "Evaporasi"), + const SizedBox(height: 20), + _buildDashboardCard(context, "Kecepatan Angin"), + const SizedBox(height: 20), + _buildDashboardCard(context, "Kualitas Udara"), + ], + ), + ), + ), + ); + } + + /// DATA BLOCK - PREVIEW DATA HARI INI + Widget _buildDataBlock( + String title, + String value, + String unit, + Color color, + IconData icon, + ) { + return Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color, width: 2), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(icon, color: color, size: 28), + const SizedBox(height: 8), + Text( + title, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 6), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + value, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: color, + ), + ), + const SizedBox(width: 4), + Text( + unit, + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade700, + ), + ), + ], + ), + ], + ), + ), + ); + } + + /// DRAWER ITEM + Widget _buildDrawerItem( + BuildContext context, + String title, + IconData icon, + ) { + return ListTile( + leading: Icon(icon), + title: Text(title), + onTap: () { + Navigator.pop(context); + Widget screen; + if (title == "Evaporasi") { + screen = const EvaporasiMonitoringScreen(); + } else if (title == "Kecepatan Angin") { + screen = const WindSpeedMonitoringScreen(); + } else if (title == "Kualitas Udara") { + screen = const AirQualityMonitoringScreen(); + } else { + return; + } + Navigator.push( + context, + MaterialPageRoute(builder: (context) => screen), + ); + }, + ); + } + + /// DASHBOARD CARD - PREVIEW GRAFIK 24 JAM + Widget _buildDashboardCard(BuildContext context, String title) { + return GestureDetector( + onTap: () { + Widget screen; + if (title == "Evaporasi") { + screen = const EvaporasiMonitoringScreen(); + } else if (title == "Kecepatan Angin") { + screen = const WindSpeedMonitoringScreen(); + } else if (title == "Kualitas Udara") { + screen = const AirQualityMonitoringScreen(); + } else { + return; + } + Navigator.push( + context, + MaterialPageRoute(builder: (context) => screen), + ); + }, + child: 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: [ + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + "24 Jam", + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Colors.green, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + SizedBox( + height: 200, + 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: 3, + getTitlesWidget: (value, meta) { + return Text( + '${value.toInt()}h', + 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: _getMonitoringData(title), + isCurved: true, + barWidth: 2.5, + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 4, + color: _getColorByTitle(title), + strokeWidth: 0, + ); + }, + ), + color: _getColorByTitle(title), + belowBarData: BarAreaData( + show: true, + color: _getColorByTitle(title).withOpacity(0.1), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Rata-rata: ${_getAverageData(title)}", + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade700, + fontWeight: FontWeight.w600, + ), + ), + Text( + "Tap untuk detail", + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + /// GET MONITORING DATA FOR 24 HOURS + List _getMonitoringData(String title) { + final dataMap = { + "Evaporasi": [ + 2.1, 2.3, 2.0, 1.9, 2.2, 2.5, 3.0, 3.5, 4.0, 4.2, + 4.5, 4.8, 5.0, 5.2, 5.1, 4.9, 4.5, 4.0, 3.5, 3.0, 2.8, 2.5, 2.3, 2.2 + ], + "Kecepatan Angin": [ + 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 + ], + "Kualitas Udara": [ + 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 + ], + }; + + final data = dataMap[title] ?? []; + return List.generate( + data.length, + (index) => FlSpot(index.toDouble(), data[index]), + ); + } + + /// GET AVERAGE VALUE + String _getAverageData(String title) { + final dataMap = { + "Evaporasi": "3.5 mm", + "Kecepatan Angin": "6.8 km/h", + "Kualitas Udara": "70 AQI", + }; + return dataMap[title] ?? "N/A"; + } + + /// GET COLOR BY TITLE + Color _getColorByTitle(String title) { + switch (title) { + case "Evaporasi": + return Colors.blue; + case "Kecepatan Angin": + return Colors.green; + case "Kualitas Udara": + return Colors.orange; + default: + return Colors.grey; + } + } +} diff --git a/lib/screens/home/views/home_screen.dart b/lib/screens/home/views/home_screen.dart deleted file mode 100644 index 0ebe19b..0000000 --- a/lib/screens/home/views/home_screen.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter/material.dart'; - -class HomeScreen extends StatelessWidget{ - const HomeScreen ({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - - ); - } -} \ No newline at end of file diff --git a/lib/screens/monitoring/air_quality_monitoring_screen.dart b/lib/screens/monitoring/air_quality_monitoring_screen.dart new file mode 100644 index 0000000..32059d7 --- /dev/null +++ b/lib/screens/monitoring/air_quality_monitoring_screen.dart @@ -0,0 +1,677 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'widgets/monitoring_shared.dart'; + +/// AIR QUALITY MONITORING SCREEN +class AirQualityMonitoringScreen extends StatefulWidget { + const AirQualityMonitoringScreen({super.key}); + + @override + State createState() => _AirQualityMonitoringScreenState(); +} + +class _AirQualityMonitoringScreenState extends State { + String _selectedPeriod = "Hari Ini"; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Monitoring Kualitas Udara"), + centerTitle: true, + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Kualitas Udara", + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), + + /// GRAFIK MONITORING + _buildMonitoringGraphBlock(), + const SizedBox(height: 25), + + /// PENJELASAN + _buildAirQualityExplanation(), + const SizedBox(height: 25), + + /// STATISTIK TAHUNAN + _buildAnnualAirQualityGraphSection(), + ], + ), + ), + ), + ); + } + + /// EXPORT FUNCTIONS + void _exportDailyAirQualityData() { + final StringBuffer csvContent = StringBuffer(); + csvContent.writeln("DATA MONITORING KUALITAS UDARA - HARIAN (24 JAM)"); + csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}"); + csvContent.writeln(""); + 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 + ]; + for (int i = 0; i < dailyData.length; i++) { + csvContent.writeln("$i:00,${dailyData[i]}"); + } + + csvContent.writeln(""); + csvContent.writeln("Statistik Harian"); + csvContent.writeln("Total: 1709 AQI"); + csvContent.writeln("Rata-rata: 71.2 AQI"); + csvContent.writeln("Maksimal: 80 AQI"); + csvContent.writeln("Minimal: 58 AQI"); + + showExportPreview(context, csvContent.toString(), "Data Harian Kualitas Udara"); + } + + void _exportWeeklyAirQualityData() { + final StringBuffer csvContent = StringBuffer(); + csvContent.writeln("DATA MONITORING KUALITAS UDARA - MINGGUAN"); + csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}"); + csvContent.writeln(""); + csvContent.writeln("Hari,Rata-rata AQI"); + + 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++) { + csvContent.writeln("${days[i]},${weeklyData[i]}"); + } + + csvContent.writeln(""); + csvContent.writeln("Statistik Mingguan"); + csvContent.writeln("Total: 495 AQI"); + csvContent.writeln("Rata-rata: 70.7 AQI"); + csvContent.writeln("Maksimal: 78 AQI"); + csvContent.writeln("Minimal: 62 AQI"); + + 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(""); + csvContent.writeln("Tanggal,Rata-rata AQI"); + + for (int i = 1; i <= 28; i++) { + final value = (70.0 + (i % 6) * 2.5).toStringAsFixed(1); + csvContent.writeln("$i,$value"); + } + + csvContent.writeln(""); + csvContent.writeln("Statistik Bulanan"); + csvContent.writeln("Total: 1990 AQI"); + csvContent.writeln("Rata-rata: 71.1 AQI"); + csvContent.writeln("Maksimal: 82 AQI"); + csvContent.writeln("Minimal: 55 AQI"); + + showExportPreview(context, csvContent.toString(), "Data Bulanan Kualitas Udara"); + } + + void _exportAnnualAirQualityData() { + final StringBuffer csvContent = StringBuffer(); + csvContent.writeln("DATA MONITORING KUALITAS UDARA - TAHUNAN"); + csvContent.writeln("Tahun: 2025"); + csvContent.writeln(""); + csvContent.writeln("Bulan,Rata-rata AQI,AQI Max"); + + final months = [ + '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]; + + for (int i = 0; i < months.length; i++) { + csvContent.writeln("${months[i]},${averages[i]},${maxAqi[i]}"); + } + + csvContent.writeln(""); + csvContent.writeln("STATISTIK TAHUNAN"); + csvContent.writeln("Rata-rata Tahunan: 74.2 AQI"); + csvContent.writeln("AQI Max Teringgi: 102.0"); + csvContent.writeln("AQI Min Terendah: 65.0"); + + showExportPreview(context, csvContent.toString(), "Data Tahunan Kualitas Udara"); + } + + /// DATA GENERATORS + List _getAirQualityPeriodData() { + switch (_selectedPeriod) { + 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 + ]; + return FlSpot(i.toDouble(), values[i]); + }); + case "Minggu Ini": + return [ + const FlSpot(0, 68.0), + const FlSpot(1, 72.0), + const FlSpot(2, 75.0), + const FlSpot(3, 78.0), + const FlSpot(4, 72.0), + const FlSpot(5, 68.0), + const FlSpot(6, 62.0), + ]; + case "Bulan Ini": + return [ + const FlSpot(0, 70.0), + const FlSpot(1, 72.5), + const FlSpot(2, 75.0), + const FlSpot(3, 77.5), + const FlSpot(4, 80.0), + const FlSpot(5, 82.0), + const FlSpot(6, 80.0), + const FlSpot(7, 78.0), + const FlSpot(8, 75.0), + const FlSpot(9, 72.0), + const FlSpot(10, 70.0), + const FlSpot(11, 68.0), + ]; + default: + return []; + } + } + + /// UI BUILDERS + Widget _buildMonitoringGraphBlock() { + 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 Kualitas Udara", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + decoration: BoxDecoration( + color: Colors.amber.shade100, + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + "Normal", + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Colors.amber, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + /// PERIOD SELECTOR + EXPORT + 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) { + setState(() { + _selectedPeriod = period; + }); + }, + selectedColor: Colors.orange, + labelStyle: TextStyle( + color: _selectedPeriod == period + ? Colors.white + : Colors.black, + ), + ), + ); + }).toList(), + ), + ), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () { + if (_selectedPeriod == "Hari Ini") { + _exportDailyAirQualityData(); + } else if (_selectedPeriod == "Minggu Ini") { + _exportWeeklyAirQualityData(); + } else if (_selectedPeriod == "Bulan Ini") { + _exportMonthlyAirQualityData(); + } + }, + icon: const Icon(Icons.download, size: 16), + label: const Text("Export"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + textStyle: const TextStyle(fontSize: 11), + ), + ), + ], + ), + const SizedBox(height: 16), + + /// GRAPH + 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: _getAirQualityPeriodData(), + isCurved: true, + barWidth: 2.5, + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 4, + color: Colors.orange, + strokeWidth: 0, + ); + }, + ), + color: Colors.orange, + belowBarData: BarAreaData( + show: true, + color: Colors.orange.withOpacity(0.1), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildAirQualityExplanation() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.orange.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Penjelasan Kualitas Udara", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.orange, + ), + ), + const SizedBox(height: 10), + const Text( + "Kualitas udara diukur menggunakan Air Quality Index (AQI) yang menunjukkan tingkat polusi udara. AQI berkisar dari 0-500, dengan nilai lebih rendah menunjukkan udara lebih bersih. Monitoring kualitas udara penting untuk kesehatan masyarakat, perencanaan lingkungan, dan identifikasi sumber polusi. Kategori: 0-50 (Baik), 51-100 (Wajar), 101-150 (Tidak Sehat), 151+ (Sangat Tidak Sehat).", + style: TextStyle( + fontSize: 12, + height: 1.6, + color: Colors.orange, + ), + ), + ], + ), + ); + } + + Widget _buildAnnualAirQualityGraphSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Statistik Tahunan Kualitas Udara", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 15), + + /// STATS + Row( + children: [ + Expanded( + child: StatCard( + title: "Rata-rata Tahunan", + value: "74.2 AQI", + color: Colors.orange, + icon: Icons.cloud, + ), + ), + const SizedBox(width: 10), + Expanded( + child: StatCard( + title: "AQI Maksimal", + value: "102.0", + color: Colors.red, + icon: Icons.warning, + ), + ), + ], + ), + const SizedBox(height: 20), + + /// CHART + const Text( + "Grafik Tahunan - Rata-rata & AQI 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: 5, + getDrawingHorizontalLine: (value) { + return FlLine( + color: Colors.grey.shade300, + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + final months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', + 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des' + ]; + return Text( + months[value.toInt()], + style: const TextStyle( + fontSize: 10, + color: Colors.grey, + ), + ); + }, + reservedSize: 30, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + return Text( + '${value.toInt()} AQI', + style: const TextStyle( + fontSize: 9, + color: Colors.grey, + ), + ); + }, + reservedSize: 50, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: 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: [ + /// Rata-rata + LineChartBarData( + spots: const [ + FlSpot(0, 68.0), + FlSpot(1, 70.0), + FlSpot(2, 72.5), + FlSpot(3, 75.0), + FlSpot(4, 78.0), + FlSpot(5, 80.0), + FlSpot(6, 82.0), + FlSpot(7, 80.0), + FlSpot(8, 75.0), + FlSpot(9, 70.0), + FlSpot(10, 68.0), + FlSpot(11, 65.0), + ], + isCurved: true, + barWidth: 2.5, + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 4, + color: Colors.orange, + strokeWidth: 0, + ); + }, + ), + color: Colors.orange, + belowBarData: BarAreaData( + show: true, + color: Colors.orange.withOpacity(0.1), + ), + ), + /// AQI Max + LineChartBarData( + spots: const [ + FlSpot(0, 85.0), + FlSpot(1, 88.0), + FlSpot(2, 90.0), + FlSpot(3, 95.0), + FlSpot(4, 98.0), + FlSpot(5, 100.0), + FlSpot(6, 102.0), + FlSpot(7, 100.0), + FlSpot(8, 95.0), + FlSpot(9, 90.0), + FlSpot(10, 85.0), + FlSpot(11, 80.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), + + /// LEGEND + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: Colors.orange, + borderRadius: BorderRadius.circular(3), + ), + ), + const SizedBox(width: 8), + const Text( + "Rata-rata AQI", + 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( + "AQI Maksimal", + style: TextStyle(fontSize: 12), + ), + ], + ), + const SizedBox(height: 15), + Center( + child: ElevatedButton.icon( + onPressed: () => _exportAnnualAirQualityData(), + 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/evaporasi_monitoring_screen.dart b/lib/screens/monitoring/evaporasi_monitoring_screen.dart new file mode 100644 index 0000000..9618d26 --- /dev/null +++ b/lib/screens/monitoring/evaporasi_monitoring_screen.dart @@ -0,0 +1,741 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'widgets/monitoring_shared.dart'; + +/// EVAPORASI MONITORING SCREEN +class EvaporasiMonitoringScreen extends StatefulWidget { + const EvaporasiMonitoringScreen({super.key}); + + @override + State createState() => _EvaporasiMonitoringScreenState(); +} + +class _EvaporasiMonitoringScreenState extends State { + String _selectedPeriod = "Hari Ini"; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Monitoring Evaporasi"), + centerTitle: true, + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Evaporasi", + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), + + /// SISTEM KONTROL AIR OTOMATIS + _buildAutomaticWaterControlSystem(), + const SizedBox(height: 25), + + /// GRAFIK MONITORING + _buildMonitoringGraphBlock(), + const SizedBox(height: 25), + + /// PENJELASAN + _buildMonitoringExplanation(), + const SizedBox(height: 25), + + /// STATISTIK TAHUNAN + _buildAnnualCombinedGraphSection(), + ], + ), + ), + ), + ); + } + + /// EXPORT FUNCTIONS + void _exportDailyData() { + final StringBuffer csvContent = StringBuffer(); + csvContent.writeln("DATA MONITORING EVAPORASI - HARIAN (24 JAM)"); + csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}"); + csvContent.writeln(""); + csvContent.writeln("Jam,Nilai (mm)"); + + final dailyData = [ + 2.1, 2.3, 2.0, 1.9, 2.2, 2.5, 3.0, 3.5, 4.0, 4.2, + 4.5, 4.8, 5.0, 5.2, 5.1, 4.9, 4.5, 4.0, 3.5, 3.0, 2.8, 2.5, 2.3, 2.2 + ]; + for (int i = 0; i < dailyData.length; i++) { + csvContent.writeln("$i:00,${dailyData[i]}"); + } + + csvContent.writeln(""); + csvContent.writeln("Statistik Harian"); + csvContent.writeln("Total: 79.4 mm"); + csvContent.writeln("Rata-rata: 3.3 mm"); + csvContent.writeln("Maksimal: 5.2 mm"); + csvContent.writeln("Minimal: 1.9 mm"); + + showExportPreview(context, csvContent.toString(), "Data Harian"); + } + + void _exportWeeklyData() { + final StringBuffer csvContent = StringBuffer(); + csvContent.writeln("DATA MONITORING EVAPORASI - MINGGUAN"); + csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}"); + csvContent.writeln(""); + csvContent.writeln("Hari,Nilai (mm)"); + + final days = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu']; + final weeklyData = [3.5, 3.8, 4.2, 4.5, 4.0, 3.8, 3.2]; + + for (int i = 0; i < days.length; i++) { + csvContent.writeln("${days[i]},${weeklyData[i]}"); + } + + csvContent.writeln(""); + csvContent.writeln("Statistik Mingguan"); + csvContent.writeln("Total: 27.0 mm"); + csvContent.writeln("Rata-rata: 3.86 mm"); + csvContent.writeln("Maksimal: 4.5 mm"); + csvContent.writeln("Minimal: 3.2 mm"); + + showExportPreview(context, csvContent.toString(), "Data Mingguan"); + } + + void _exportMonthlyData() { + final StringBuffer csvContent = StringBuffer(); + csvContent.writeln("DATA MONITORING EVAPORASI - BULANAN"); + csvContent.writeln("Bulan: ${DateTime.now().toString().split(' ')[0].substring(0, 7)}"); + csvContent.writeln(""); + csvContent.writeln("Tanggal,Nilai (mm)"); + + for (int i = 1; i <= 28; i++) { + final value = (3.0 + (i % 6) * 0.5).toStringAsFixed(1); + csvContent.writeln("$i,$value"); + } + + csvContent.writeln(""); + csvContent.writeln("Statistik Bulanan"); + csvContent.writeln("Total: 99.2 mm"); + csvContent.writeln("Rata-rata: 3.54 mm"); + csvContent.writeln("Maksimal: 5.0 mm"); + csvContent.writeln("Minimal: 2.5 mm"); + + showExportPreview(context, csvContent.toString(), "Data Bulanan"); + } + + void _exportAnnualData() { + final StringBuffer csvContent = StringBuffer(); + csvContent.writeln("DATA MONITORING EVAPORASI - TAHUNAN"); + csvContent.writeln("Tahun: 2025"); + csvContent.writeln(""); + csvContent.writeln("Bulan,Pengisian (x),Rata-rata (mm)"); + + final months = [ + 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', + 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember' + ]; + final fillings = [12, 13, 15, 14, 13, 12, 14, 16, 13, 14, 15, 13]; + final averages = [3.5, 3.8, 4.2, 4.5, 5.0, 5.2, 5.5, 5.8, 5.0, 4.5, 4.0, 3.7]; + + for (int i = 0; i < months.length; i++) { + csvContent.writeln("${months[i]},${fillings[i]},${averages[i]}"); + } + + csvContent.writeln(""); + csvContent.writeln("STATISTIK TAHUNAN"); + csvContent.writeln("Total Pengisian: 156x"); + csvContent.writeln("Rata-rata Evaporasi: 4.4 mm"); + csvContent.writeln("Total Evaporasi: 52.8 mm"); + + showExportPreview(context, csvContent.toString(), "Data Tahunan"); + } + + /// DATA GENERATORS + List _getEvaporationPeriodData() { + switch (_selectedPeriod) { + case "Hari Ini": + return List.generate(24, (i) { + final values = [ + 2.1, 2.3, 2.0, 1.9, 2.2, 2.5, 3.0, 3.5, 4.0, 4.2, + 4.5, 4.8, 5.0, 5.2, 5.1, 4.9, 4.5, 4.0, 3.5, 3.0, 2.8, 2.5, 2.3, 2.2 + ]; + return FlSpot(i.toDouble(), values[i]); + }); + case "Minggu Ini": + return [ + const FlSpot(0, 3.5), + const FlSpot(1, 3.8), + const FlSpot(2, 4.2), + const FlSpot(3, 4.5), + const FlSpot(4, 4.0), + const FlSpot(5, 3.8), + const FlSpot(6, 3.2), + ]; + case "Bulan Ini": + return [ + const FlSpot(0, 3.5), + const FlSpot(1, 3.8), + const FlSpot(2, 4.2), + const FlSpot(3, 4.5), + const FlSpot(4, 5.0), + const FlSpot(5, 5.2), + const FlSpot(6, 5.5), + const FlSpot(7, 5.8), + const FlSpot(8, 5.0), + const FlSpot(9, 4.5), + const FlSpot(10, 4.0), + const FlSpot(11, 3.7), + ]; + default: + return []; + } + } + + /// UI BUILDERS + Widget _buildAutomaticWaterControlSystem() { + bool isActive = DateTime.now().hour >= 6 && DateTime.now().hour < 7; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Sistem Kontrol Air Otomatis", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Jadwal Operasional", + style: TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + const SizedBox(height: 4), + const Text( + "06.00 - 07.00 WIB", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + decoration: BoxDecoration( + color: isActive ? Colors.blue : Colors.grey.shade300, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + isActive ? "AKTIF" : "TIDAK AKTIF", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: isActive ? Colors.white : Colors.grey.shade700, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: const Text( + "Sistem otomatis akan mengisi air pada waktu yang telah ditentukan untuk menjaga tingkat evaporasi pada level optimal.", + style: TextStyle( + fontSize: 11, + color: Colors.blue, + height: 1.5, + ), + ), + ), + ], + ), + ); + } + + Widget _buildMonitoringGraphBlock() { + 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 Evaporasi", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + decoration: BoxDecoration( + color: Colors.amber.shade100, + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + "Normal", + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Colors.amber, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + /// PERIOD SELECTOR + EXPORT + 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) { + setState(() { + _selectedPeriod = period; + }); + }, + selectedColor: Colors.blue, + labelStyle: TextStyle( + color: _selectedPeriod == period + ? Colors.white + : Colors.black, + ), + ), + ); + }).toList(), + ), + ), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () { + if (_selectedPeriod == "Hari Ini") { + _exportDailyData(); + } else if (_selectedPeriod == "Minggu Ini") { + _exportWeeklyData(); + } else if (_selectedPeriod == "Bulan Ini") { + _exportMonthlyData(); + } + }, + icon: const Icon(Icons.download, size: 16), + label: const Text("Export"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + textStyle: const TextStyle(fontSize: 11), + ), + ), + ], + ), + const SizedBox(height: 16), + + /// GRAPH + 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: _getEvaporationPeriodData(), + isCurved: true, + barWidth: 2.5, + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 4, + color: Colors.blue, + strokeWidth: 0, + ); + }, + ), + color: Colors.blue, + belowBarData: BarAreaData( + show: true, + color: Colors.blue.withOpacity(0.1), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildMonitoringExplanation() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Penjelasan Evaporasi", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + const SizedBox(height: 10), + const Text( + "Evaporasi adalah proses penguapan air dari permukaan tanah atau air menuju atmosfer. Tingkat evaporasi dipengaruhi oleh suhu, kelembaban udara, kecepatan angin, dan intensitas cahaya matahari. Data ini penting untuk manajemen irigasi dan perencanaan kebutuhan air.", + style: TextStyle( + fontSize: 12, + height: 1.6, + color: Colors.blue, + ), + ), + ], + ), + ); + } + + Widget _buildAnnualCombinedGraphSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Statistik Tahunan & Rata-rata", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 15), + + /// STATS + Row( + children: [ + Expanded( + child: StatCard( + title: "Total Pengisian", + value: "156x", + color: Colors.blue, + icon: Icons.water_drop, + ), + ), + const SizedBox(width: 10), + Expanded( + child: StatCard( + title: "Rata-rata", + value: "4.2 mm", + color: Colors.green, + icon: Icons.trending_up, + ), + ), + ], + ), + const SizedBox(height: 20), + + /// CHART + const Text( + "Grafik Tahunan - Pengisian Air & Rata-rata Evaporasi", + 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: Column( + children: [ + SizedBox( + height: 320, + child: BarChart( + BarChartData( + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + drawVerticalLine: false, + horizontalInterval: 2, + getDrawingHorizontalLine: (value) { + return FlLine( + color: Colors.grey.shade300, + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + final months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', + 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des' + ]; + return Text( + months[value.toInt()], + style: const TextStyle( + fontSize: 10, + color: Colors.grey, + ), + ); + }, + reservedSize: 30, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + return Text( + value.toInt().toString(), + style: const TextStyle( + fontSize: 9, + color: Colors.grey, + ), + ); + }, + reservedSize: 28, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + return Text( + '${value.toInt()} mm', + style: const TextStyle( + fontSize: 9, + color: Colors.green, + ), + ); + }, + reservedSize: 45, + ), + ), + topTitles: 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, + ), + right: BorderSide( + color: Colors.grey.shade300, + width: 1, + ), + ), + ), + barGroups: [ + buildBarGroup(0, 12), + buildBarGroup(1, 13), + buildBarGroup(2, 15), + buildBarGroup(3, 14), + buildBarGroup(4, 13), + buildBarGroup(5, 12), + buildBarGroup(6, 14), + buildBarGroup(7, 16), + buildBarGroup(8, 13), + buildBarGroup(9, 14), + buildBarGroup(10, 15), + buildBarGroup(11, 13), + ], + rangeAnnotations: RangeAnnotations( + horizontalRangeAnnotations: [ + HorizontalRangeAnnotation( + y1: 3.5, + y2: 5.8, + color: Colors.green.withOpacity(0.1), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(3), + ), + ), + const SizedBox(width: 8), + const Text( + "Pengisian Air (kali)", + style: TextStyle(fontSize: 12), + ), + const SizedBox(width: 20), + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.3), + borderRadius: BorderRadius.circular(3), + ), + ), + const SizedBox(width: 8), + const Text( + "Area Rata-rata (mm)", + style: TextStyle(fontSize: 12), + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 15), + Center( + child: ElevatedButton.icon( + onPressed: () => _exportAnnualData(), + 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/widgets/monitoring_shared.dart b/lib/screens/monitoring/widgets/monitoring_shared.dart new file mode 100644 index 0000000..db78b10 --- /dev/null +++ b/lib/screens/monitoring/widgets/monitoring_shared.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; + +/// SHOW EXPORT PREVIEW DIALOG +/// Helper function untuk menampilkan preview export data +void showExportPreview(BuildContext context, String content, String title) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text("Preview Export - $title"), + content: SizedBox( + width: double.maxFinite, + child: SingleChildScrollView( + child: Text( + content, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 10, + ), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text("Tutup"), + ), + ], + ), + ); +} + +/// STAT CARD WIDGET +/// Reusable card untuk menampilkan statistik +class StatCard extends StatelessWidget { + final String title; + final String value; + final Color color; + final IconData icon; + + const StatCard({ + super.key, + required this.title, + required this.value, + required this.color, + required this.icon, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color, width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, color: color, size: 20), + const SizedBox(height: 8), + Text( + title, + style: const TextStyle( + fontSize: 11, + color: Colors.grey, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), + ); + } +} + +/// BUILD BAR GROUP FOR CHART +/// Helper untuk membuat bar group di BarChart +BarChartGroupData buildBarGroup(int x, double value) { + return BarChartGroupData( + x: x, + barRods: [ + BarChartRodData( + toY: value.toDouble(), + color: Colors.blue, + width: 12, + borderRadius: const BorderRadius.vertical(top: Radius.circular(6)), + ), + ], + ); +} + +/// GET PERIOD INTERVAL +/// Menentukan spacing interval berdasarkan periode +double getPeriodInterval(String period) { + switch (period) { + case "Hari Ini": + return 3; + case "Minggu Ini": + return 1; + case "Bulan Ini": + return 2; + default: + return 1; + } +} + +/// GET PERIOD LABEL +/// Menentukan label untuk X-axis berdasarkan periode +String getPeriodLabel(String period, double value) { + switch (period) { + case "Hari Ini": + return '${value.toInt()}h'; + case "Minggu Ini": + final days = ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min']; + return days[value.toInt()]; + case "Bulan Ini": + return 'M${value.toInt() + 1}'; + default: + return ''; + } +} diff --git a/lib/screens/monitoring/wind_speed_monitoring_screen.dart b/lib/screens/monitoring/wind_speed_monitoring_screen.dart new file mode 100644 index 0000000..f56b33f --- /dev/null +++ b/lib/screens/monitoring/wind_speed_monitoring_screen.dart @@ -0,0 +1,677 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'widgets/monitoring_shared.dart'; + +/// WIND SPEED MONITORING SCREEN +class WindSpeedMonitoringScreen extends StatefulWidget { + const WindSpeedMonitoringScreen({super.key}); + + @override + State createState() => _WindSpeedMonitoringScreenState(); +} + +class _WindSpeedMonitoringScreenState extends State { + String _selectedPeriod = "Hari Ini"; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Monitoring Kecepatan Angin"), + centerTitle: true, + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Kecepatan Angin", + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), + + /// GRAFIK MONITORING + _buildMonitoringGraphBlock(), + const SizedBox(height: 25), + + /// PENJELASAN + _buildWindSpeedExplanation(), + const SizedBox(height: 25), + + /// STATISTIK TAHUNAN + _buildAnnualWindGraphSection(), + ], + ), + ), + ), + ); + } + + /// EXPORT FUNCTIONS + 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 = [ + 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 + ]; + 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 = [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]; + + 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"); + } + + /// DATA GENERATORS + List _getWindSpeedPeriodData() { + switch (_selectedPeriod) { + case "Hari Ini": + return List.generate(24, (i) { + final values = [ + 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 + ]; + return FlSpot(i.toDouble(), values[i]); + }); + case "Minggu Ini": + return [ + const FlSpot(0, 6.5), + const FlSpot(1, 7.2), + const FlSpot(2, 7.8), + const FlSpot(3, 8.0), + const FlSpot(4, 7.5), + const FlSpot(5, 6.8), + const FlSpot(6, 5.5), + ]; + case "Bulan Ini": + return [ + const FlSpot(0, 6.0), + const FlSpot(1, 6.5), + const FlSpot(2, 7.0), + const FlSpot(3, 7.5), + const FlSpot(4, 7.8), + const FlSpot(5, 8.0), + const FlSpot(6, 8.2), + const FlSpot(7, 8.0), + const FlSpot(8, 7.5), + const FlSpot(9, 7.0), + const FlSpot(10, 6.5), + const FlSpot(11, 6.0), + ]; + default: + return []; + } + } + + /// UI BUILDERS + Widget _buildMonitoringGraphBlock() { + 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: Colors.amber.shade100, + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + "Normal", + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Colors.amber, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + /// PERIOD SELECTOR + EXPORT + 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) { + setState(() { + _selectedPeriod = period; + }); + }, + selectedColor: Colors.green, + labelStyle: TextStyle( + color: _selectedPeriod == period + ? Colors.white + : Colors.black, + ), + ), + ); + }).toList(), + ), + ), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () { + if (_selectedPeriod == "Hari Ini") { + _exportDailyWindData(); + } else if (_selectedPeriod == "Minggu Ini") { + _exportWeeklyWindData(); + } else if (_selectedPeriod == "Bulan Ini") { + _exportMonthlyWindData(); + } + }, + icon: const Icon(Icons.download, size: 16), + label: const Text("Export"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + textStyle: const TextStyle(fontSize: 11), + ), + ), + ], + ), + const SizedBox(height: 16), + + /// GRAPH + 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: _getWindSpeedPeriodData(), + 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), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildWindSpeedExplanation() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.green.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Penjelasan Kecepatan Angin", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + const SizedBox(height: 10), + const Text( + "Kecepatan angin adalah laju gerakan massa udara yang mengalir secara horizontal. Data kecepatan angin penting untuk prediksi cuaca, manajemen rawan angin, perencanaan energi terbarukan (angin), serta membantu dalam pengeringan hasil pertanian. Pengukuran dilakukan dalam satuan km/h.", + style: TextStyle( + fontSize: 12, + height: 1.6, + color: Colors.green, + ), + ), + ], + ), + ); + } + + Widget _buildAnnualWindGraphSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Statistik Tahunan Kecepatan Angin", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 15), + + /// STATS + Row( + children: [ + Expanded( + child: StatCard( + title: "Rata-rata Tahunan", + value: "7.1 km/h", + color: Colors.green, + icon: Icons.air, + ), + ), + const SizedBox(width: 10), + Expanded( + child: StatCard( + title: "Kecepatan Max", + value: "16.5 km/h", + color: Colors.red, + icon: Icons.trending_up, + ), + ), + ], + ), + const SizedBox(height: 20), + + /// CHART + 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: Colors.grey.shade300, + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + final months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', + 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des' + ]; + return Text( + months[value.toInt()], + style: const TextStyle( + fontSize: 10, + color: Colors.grey, + ), + ); + }, + reservedSize: 30, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + return Text( + '${value.toInt()} km/h', + style: const TextStyle( + fontSize: 9, + color: Colors.grey, + ), + ); + }, + reservedSize: 50, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: 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: [ + /// Rata-rata + 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), + ), + ), + /// Max Speed + 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), + + /// LEGEND + 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: () => _exportAnnualWindData(), + icon: const Icon(Icons.file_download), + label: const Text("Download Excel Tahunan"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ), + ), + ], + ); + } +} diff --git a/packages/user_repository/pubspec.lock b/packages/user_repository/pubspec.lock index 3185b3d..6260b95 100644 --- a/packages/user_repository/pubspec.lock +++ b/packages/user_repository/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7" + sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182 url: "https://pub.dev" source: hosted - version: "1.3.35" + version: "1.3.66" async: dependency: transitive description: @@ -45,26 +45,26 @@ packages: dependency: "direct main" description: name: cloud_firestore - sha256: a0f161b92610e078b4962d7e6ebeb66dc9cce0ada3514aeee442f68165d78185 + sha256: "54484b2fc49f41b46f35b60a54b12351181eeaad22c0e3def276a81e17ae7c9b" url: "https://pub.dev" source: hosted - version: "4.17.5" + version: "6.1.2" cloud_firestore_platform_interface: dependency: transitive description: name: cloud_firestore_platform_interface - sha256: "6a55b319f8d33c307396b9104512e8130a61904528ab7bd8b5402678fca54b81" + sha256: dfaa8b2c0d0a824af289d4159816a5c78417feec264c2194081d645687195158 url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "7.0.6" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web - sha256: "89dfa1304d3da48b3039abbb2865e3d30896ef858e569a16804a99f4362283a9" + sha256: "35d01f502b3b701d700470d32a8f82704dac8341a66e86c074900cde5bab343d" url: "https://pub.dev" source: hosted - version: "3.12.5" + version: "5.1.2" collection: dependency: transitive description: @@ -85,50 +85,50 @@ packages: dependency: "direct main" description: name: firebase_auth - sha256: cfc2d970829202eca09e2896f0a5aa7c87302817ecc0bdfa954f026046bf10ba + sha256: b20d1540460814c5984474c1e9dd833bdbcff6ecd8d6ad86cc9da8cfd581c172 url: "https://pub.dev" source: hosted - version: "4.20.0" + version: "6.1.4" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: a0270e1db3b2098a14cb2a2342b3cd2e7e458e0c391b1f64f6f78b14296ec093 + sha256: fd0225320b6bbc92460c86352d16b60aea15f9ef88292774cca97b0522ea9f72 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "8.1.6" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: "64e067e763c6378b7e774e872f0f59f6812885e43020e25cde08f42e9459837b" + sha256: be7dccb263b89fbda2a564de9d8193118196e8481ffb937222a025cdfdf82c40 url: "https://pub.dev" source: hosted - version: "5.12.0" + version: "6.1.2" firebase_core: dependency: transitive description: name: firebase_core - sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c" + sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80" url: "https://pub.dev" source: hosted - version: "2.32.0" + version: "4.4.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "8bcfad6d7033f5ea951d15b867622a824b13812178bfec0c779b9d81de011bbb" + sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 url: "https://pub.dev" source: hosted - version: "5.4.2" + version: "6.0.2" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "362e52457ed2b7b180964769c1e04d1e0ea0259fdf7025fdfedd019d4ae2bd88" + sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084" url: "https://pub.dev" source: hosted - version: "2.17.5" + version: "3.4.0" flutter: dependency: "direct main" description: flutter @@ -138,10 +138,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "3.0.2" flutter_test: dependency: "direct dev" description: flutter @@ -152,6 +152,14 @@ packages: description: flutter source: sdk version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" http_parser: dependency: transitive description: @@ -188,10 +196,10 @@ packages: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.0.0" matcher: dependency: transitive description: @@ -321,10 +329,10 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.1" sdks: dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.22.0" diff --git a/pubspec.lock b/pubspec.lock index 3b75fe1..7c518d0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -153,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d" + url: "https://pub.dev" + source: hosted + version: "0.66.2" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 81e0163..f82e774 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,6 +7,11 @@ environment: dependencies: flutter: sdk: flutter +<<<<<<< Updated upstream +======= + fl_chart: ^0.66.0 + +>>>>>>> Stashed changes firebase_core: ^4.3.0 firebase_auth: ^6.1.3 cloud_firestore: ^6.1.1