This commit is contained in:
kleponijo 2026-04-12 17:28:16 +07:00
parent 015ff0da93
commit 60b2047eae
15 changed files with 98 additions and 2413 deletions

View File

@ -1,780 +1,10 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
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.dart';
import '../../monitoring/air_quality_monitoring_screen.dart';
class HomeScreen extends StatefulWidget {
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
// simple model to represent one activity log item
class ActivityItem {
final String type; // evaporasi, angin, udara, etc
final String message;
final DateTime time;
ActivityItem({
required this.type,
required this.message,
required this.time,
});
}
class _HomeScreenState extends State<HomeScreen> {
// Firebase realtime database references
final DatabaseReference _evaporasiRef =
FirebaseDatabase.instance.ref('kelompok1/evaporasi');
final DatabaseReference _windSpeedRef =
FirebaseDatabase.instance.ref('anemometer/realtime');
final DatabaseReference _airQualityRef =
FirebaseDatabase.instance.ref('air_quality/realtime');
// Stream subscriptions
StreamSubscription<DatabaseEvent>? _evaporasiSub;
StreamSubscription<DatabaseEvent>? _windSpeedSub;
StreamSubscription<DatabaseEvent>? _airQualitySub;
// Realtime values
double _currentEvaporasi = 0.0;
double _currentWindSpeed = 0.0;
double _currentAirQuality = 0.0;
DateTime? _evaporasiUpdateTime;
DateTime? _windSpeedUpdateTime;
DateTime? _airQualityUpdateTime;
// Hourly data for charts (24 hours)
final List<double> _evaporasiData = List<double>.filled(24, 0.0);
final List<double> _windSpeedData = List<double>.filled(24, 0.0);
final List<double> _airQualityData = List<double>.filled(24, 0.0);
// activity log
final List<ActivityItem> _activities = [];
// Hourly aggregation helpers
double _evaporasiHourlyTotal = 0.0;
int _evaporasiHourlyCount = 0;
double _windSpeedHourlyTotal = 0.0;
int _windSpeedHourlyCount = 0;
double _airQualityHourlyTotal = 0.0;
int _airQualityHourlyCount = 0;
int _currentHour = DateTime.now().hour;
@override
void initState() {
super.initState();
_evaporasiSub = _evaporasiRef.onValue.listen(_onEvaporasiData);
_windSpeedSub = _windSpeedRef.onValue.listen(_onWindSpeedData);
_airQualitySub = _airQualityRef.onValue.listen(_onAirQualityData);
}
void _onEvaporasiData(DatabaseEvent event) {
final data = event.snapshot.value;
if (data is Map) {
final value = double.tryParse(
(data['evaporasi'] ?? data['nilai'] ?? 0).toString()) ??
0.0;
DateTime waktu;
if (data.containsKey('timestamp')) {
final timestamp =
int.tryParse(data['timestamp']?.toString() ?? '0') ?? 0;
waktu = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
} else {
final hour = int.tryParse(data['jam']?.toString() ?? '0') ?? 0;
final minute = int.tryParse(data['menit']?.toString() ?? '0') ?? 0;
final now = DateTime.now();
waktu = DateTime(now.year, now.month, now.day, hour, minute);
}
setState(() {
_currentEvaporasi = value;
_evaporasiUpdateTime = waktu;
// add to activity log
_activities.insert(
0,
ActivityItem(
type: "evaporasi",
message: "Evaporasi tercatat ${value.toStringAsFixed(1)} mm",
time: waktu,
),
);
if (_activities.length > 50) _activities.removeLast();
final incomingHour = waktu.hour;
if (incomingHour != _currentHour) {
if (_evaporasiHourlyCount > 0) {
final avg = _evaporasiHourlyTotal / _evaporasiHourlyCount;
_evaporasiData[_currentHour] = avg;
}
_evaporasiHourlyTotal = 0.0;
_evaporasiHourlyCount = 0;
_currentHour = incomingHour;
if (incomingHour == 0) {
for (int i = 0; i < 24; i++) {
_evaporasiData[i] = 0.0;
}
}
}
_evaporasiHourlyTotal += value;
_evaporasiHourlyCount++;
});
}
}
void _onWindSpeedData(DatabaseEvent event) {
final data = event.snapshot.value;
if (data is Map) {
final value = (data['kecepatan'] ?? 0).toDouble();
final timestamp = data['timestamp'] ?? 0;
final waktu = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
setState(() {
_currentWindSpeed = value;
_windSpeedUpdateTime = waktu;
// add to activity log
_activities.insert(
0,
ActivityItem(
type: "angin",
message:
"Kecepatan angin update menjadi ${value.toStringAsFixed(1)} km/h",
time: waktu,
),
);
if (_activities.length > 50) _activities.removeLast();
final incomingHour = waktu.hour;
if (incomingHour != _currentHour) {
if (_windSpeedHourlyCount > 0) {
final avg = _windSpeedHourlyTotal / _windSpeedHourlyCount;
_windSpeedData[_currentHour] = avg;
}
_windSpeedHourlyTotal = 0.0;
_windSpeedHourlyCount = 0;
_currentHour = incomingHour;
if (incomingHour == 0) {
for (int i = 0; i < 24; i++) {
_windSpeedData[i] = 0.0;
}
}
}
_windSpeedHourlyTotal += value;
_windSpeedHourlyCount++;
});
}
}
void _onAirQualityData(DatabaseEvent event) {
final data = event.snapshot.value;
if (data is Map) {
final value = (data['aqi'] ?? 0).toDouble();
final timestamp = data['timestamp'] ?? 0;
final waktu = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
setState(() {
_currentAirQuality = value;
_airQualityUpdateTime = waktu;
// add to activity log
_activities.insert(
0,
ActivityItem(
type: "udara",
message: "AQI berubah menjadi ${value.toStringAsFixed(0)}",
time: waktu,
),
);
if (_activities.length > 50) _activities.removeLast();
final incomingHour = waktu.hour;
if (incomingHour != _currentHour) {
if (_airQualityHourlyCount > 0) {
final avg = _airQualityHourlyTotal / _airQualityHourlyCount;
_airQualityData[_currentHour] = avg;
}
_airQualityHourlyTotal = 0.0;
_airQualityHourlyCount = 0;
_currentHour = incomingHour;
if (incomingHour == 0) {
for (int i = 0; i < 24; i++) {
_airQualityData[i] = 0.0;
}
}
}
_airQualityHourlyTotal += value;
_airQualityHourlyCount++;
});
}
}
@override
void dispose() {
_evaporasiSub?.cancel();
_windSpeedSub?.cancel();
_airQualitySub?.cancel();
super.dispose();
}
@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 (dynamic dari AuthenticationBloc)
BlocBuilder<AuthenticationBloc, AuthenticationState>(
builder: (context, state) {
final user = state.user;
final displayName = (user != null && user.name.isNotEmpty)
? user.name
: 'Pengguna';
final email = (user != null && user.email.isNotEmpty)
? user.email
: 'Tidak ada email';
final initials = (displayName.isNotEmpty)
? displayName
.trim()
.split(' ')
.map((e) => e.isNotEmpty ? e[0] : '')
.take(2)
.join()
: 'U';
return UserAccountsDrawerHeader(
accountName: Text(
displayName,
style: const TextStyle(fontWeight: FontWeight.bold),
),
accountEmail: Text(email),
currentAccountPicture: CircleAvatar(
backgroundColor: Colors.blue,
child: Text(
initials.toUpperCase(),
style: const 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,
),
const Divider(),
ListTile(
leading: const Icon(Icons.logout),
title: const Text('Logout'),
onTap: () async {
Navigator.pop(context);
// trigger logout via bloc
final bloc = context.read<AuthenticationBloc>();
bloc.add(const AuthenticationLogoutRequested());
},
),
],
),
),
),
/// 🔹 DASHBOARD UTAMA - 3 MONITORING
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BlocBuilder<AuthenticationBloc, AuthenticationState>(
builder: (context, state) {
final name = state.user?.name.isNotEmpty == true
? state.user!.name
: 'Pengguna';
return Text(
'Selamat Datang $name',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
);
},
),
const SizedBox(height: 10),
const Text(
"Recent Activity",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Expanded(
child: _activities.isEmpty
? const Center(child: Text("Belum ada aktivitas"))
: ListView.builder(
itemCount: _activities.length,
itemBuilder: (context, index) {
final item = _activities[index];
return _buildActivityTile(item);
},
),
),
],
),
),
);
}
/// 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<FlSpot> _getMonitoringData(String title) {
late final List<double> data;
switch (title) {
case "Evaporasi":
data = _evaporasiData;
break;
case "Kecepatan Angin":
data = _windSpeedData;
break;
case "Kualitas Udara":
data = _airQualityData;
break;
default:
data = [];
}
return List.generate(
data.length,
(index) => FlSpot(index.toDouble(), data[index]),
);
}
/// GET AVERAGE VALUE
String _getAverageData(String title) {
late final double average;
late final String unit;
switch (title) {
case "Evaporasi":
average = _evaporasiData.isNotEmpty
? _evaporasiData.reduce((a, b) => a + b) / _evaporasiData.length
: 0.0;
unit = "mm";
break;
case "Kecepatan Angin":
average = _windSpeedData.isNotEmpty
? _windSpeedData.reduce((a, b) => a + b) / _windSpeedData.length
: 0.0;
unit = "km/h";
break;
case "Kualitas Udara":
average = _airQualityData.isNotEmpty
? _airQualityData.reduce((a, b) => a + b) / _airQualityData.length
: 0.0;
unit = "AQI";
break;
default:
return "N/A";
}
return "${average.toStringAsFixed(1)} $unit";
}
/// 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;
}
}
/// ACTIVITY TILE
Widget _buildActivityTile(ActivityItem item) {
IconData icon;
Color color;
switch (item.type) {
case "evaporasi":
icon = Icons.opacity;
color = Colors.blue;
break;
case "angin":
icon = Icons.air;
color = Colors.green;
break;
case "udara":
icon = Icons.cloud;
color = Colors.orange;
break;
default:
icon = Icons.info;
color = Colors.grey;
}
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: CircleAvatar(
backgroundColor: color.withOpacity(0.2),
child: Icon(icon, color: color),
),
title: Text(item.message),
subtitle: Text(
"${item.time.hour}:${item.time.minute.toString().padLeft(2, '0')}",
),
),
);
return Scaffold();
}
}

View File

@ -1,15 +1,44 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
class WindSpeedBloc {
final MonitoringRepository _repo;
part 'wind_speed_event.dart';
part 'wind_speed_state.dart';
WindSpeedBloc({required MonitoringRepository repo}) : _repo = repo;
class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
final MonitoringRepository _repository;
StreamSubscription? _subscription;
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);
WindSpeedBloc({required MonitoringRepository repository})
: _repository = repository,
super(const WindSpeedState()) {
// Handler saat aplikasi minta mulai monitoring
on<WatchWindSpeedStarted>((event, emit) {
_subscription?.cancel();
_subscription = _repository
.getSensorStream(
'anemometer/realtime', (json) => MyWindSpeed.fromJson(json))
.listen((data) => add(_WindSpeedUpdated(data)));
});
// Handler saat ada data baru masuk (Pindahan logika dari Screen)
on<_WindSpeedUpdated>((event, emit) {
// Di sini kamu bisa tambahkan logika hitung rata-rata/grafik harian
emit(state.copyWith(
currentSpeed: event.data.speed,
isLoading: false,
));
});
on<WindSpeedPeriodChanged>((event, emit) {
emit(state.copyWith(selectedPeriod: event.period));
});
}
@override
Future<void> close() {
_subscription?.cancel(); // Supaya tidak bocor memorinya
return super.close();
}
}

View File

@ -0,0 +1,24 @@
part of 'wind_speed_bloc.dart';
sealed class WindSpeedEvent extends Equatable {
const WindSpeedEvent();
@override
List<Object> get props => [];
}
// Perintah untuk mulai dengerin data Firebase
class WatchWindSpeedStarted extends WindSpeedEvent {}
// Perintah kalau user ganti filter (Hari Ini, Minggu Ini, dll)
class WindSpeedPeriodChanged extends WindSpeedEvent {
final String period;
const WindSpeedPeriodChanged(this.period);
}
// Event internal: saat data baru masuk dari Firebase
class _WindSpeedUpdated extends WindSpeedEvent {
final MyWindSpeed data;
const _WindSpeedUpdated(this.data);
@override
List<Object> get props => [data];
}

View File

@ -0,0 +1,33 @@
part of 'wind_speed_bloc.dart';
class WindSpeedState extends Equatable {
final double currentSpeed;
final String selectedPeriod;
final List<double> dailySpeeds; // Untuk grafik
final bool isLoading;
const WindSpeedState({
this.currentSpeed = 0.0,
this.selectedPeriod = "Hari Ini",
this.dailySpeeds = const [],
this.isLoading = true,
});
WindSpeedState copyWith({
double? currentSpeed,
String? selectedPeriod,
List<double>? dailySpeeds,
bool? isLoading,
}) {
return WindSpeedState(
currentSpeed: currentSpeed ?? this.currentSpeed,
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
dailySpeeds: dailySpeeds ?? this.dailySpeeds,
isLoading: isLoading ?? this.isLoading,
);
}
@override
List<Object> get props =>
[currentSpeed, selectedPeriod, dailySpeeds, isLoading];
}

View File

@ -1,226 +0,0 @@
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,
),
),
),
],
);
}
}

View File

@ -1,45 +0,0 @@
import 'package:flutter/material.dart';
class WindSpeedExplanation extends StatelessWidget {
const WindSpeedExplanation({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
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 [
Text(
"Penjelasan Kecepatan Angin",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 0, 0, 0),
),
),
SizedBox(height: 10),
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: Color.fromARGB(255, 0, 0, 0),
),
),
],
),
);
}
}

View File

@ -1,253 +0,0 @@
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<double> dailySpeeds;
final bool isOnline;
final ValueChanged<String> onPeriodChanged;
final ValueChanged<String> onExportPressed;
const WindSpeedGraphBlock({
super.key,
required this.selectedPeriod,
required this.dailySpeeds,
required this.isOnline,
required this.onPeriodChanged,
required this.onExportPressed,
});
List<FlSpot> _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),
),
),
],
),
),
),
],
),
),
);
}
}

View File

@ -1,95 +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);
/// ====== 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,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1 @@

View File

@ -1,226 +0,0 @@
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,
),
),
),
],
);
}
}

View File

@ -1,253 +0,0 @@
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<double> dailySpeeds;
final bool isOnline;
final ValueChanged<String> onPeriodChanged;
final ValueChanged<String> onExportPressed;
const WindSpeedGraphBlock({
super.key,
required this.selectedPeriod,
required this.dailySpeeds,
required this.isOnline,
required this.onPeriodChanged,
required this.onExportPressed,
});
List<FlSpot> _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),
),
),
],
),
),
),
],
),
),
);
}
}

View File

@ -1,272 +0,0 @@
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<WindSpeedMonitoringScreen> createState() =>
_WindSpeedMonitoringScreenState();
}
class _WindSpeedMonitoringScreenState extends State<WindSpeedMonitoringScreen> {
String _selectedPeriod = "Hari Ini";
// realtime values
DateTime? _lastUpdateTime;
double _currentSpeed = 0.0;
final List<double> _dailySpeeds = List<double>.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,
),
],
),
),
),
);
}
}

View File

@ -1,262 +0,0 @@
import 'dart:async';
import 'package:firebase_database/firebase_database.dart';
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<WindSpeedMonitoringScreen> createState() =>
_WindSpeedMonitoringScreenState();
}
class _WindSpeedMonitoringScreenState extends State<WindSpeedMonitoringScreen> {
String _selectedPeriod = "Hari Ini";
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,
),
],
),
),
),
);
}
}

View File

@ -2,3 +2,4 @@ library monitoring_repository;
export 'src/monitoring_repo.dart';
export 'src/firebase_monitoring_repo.dart';
export 'src/models/models.dart';

View File

@ -1,4 +1,3 @@
import 'package:firebase_database/firebase_database.dart';
import 'models/models.dart';
abstract class MonitoringRepository {