import 'package:flutter/material.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:fl_chart/fl_chart.dart'; import '../services/notif_service.dart'; import 'notifikasi_page.dart'; import 'package:intl/intl.dart'; final db = FirebaseDatabase.instanceFor( app: Firebase.app(), databaseURL: "https://tugas-akhir-9947b-default-rtdb.asia-southeast1.firebasedatabase.app", ).ref(); class BerandaPage extends StatefulWidget { const BerandaPage({super.key}); @override State createState() => _BerandaPageState(); } class _BerandaPageState extends State { @override void initState() { super.initState(); loadRiwayat(); } // ===================================================== // DATA GRAFIK // ===================================================== final List burungData = []; final List manusiaData = []; int index = 1; // ===================================================== // LAST REALTIME DATA // ===================================================== String lastJarak = "-- cm"; String lastSuara = "35"; String lastDeteksiRealtime = "--"; String pir1Status = "0"; String pir2Status = "0"; // ===================================================== // LAST VALUE GRAFIK // ===================================================== double lastBurungValue = 0; double lastManusiaValue = 0; // ===================================================== // NOTIF TIMER // ===================================================== DateTime? lastNotifTime; bool burungSudahNotif = false; bool historiLoaded = false; @override Widget build(BuildContext context) { if (!historiLoaded) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } return Scaffold( backgroundColor: const Color(0xFFF2F2F2), appBar: AppBar( title: const Text( "Beranda", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), backgroundColor: const Color(0xFF6FCF97), elevation: 0, actions: [ IconButton( icon: const Icon(Icons.notifications, color: Colors.white), onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (context) => const NotifikasiPage()), ); }, ), ], ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ // ===================================================== // STATUS SISTEM // ===================================================== StreamBuilder( stream: db.child("Kebun/Status").onValue, builder: (context, snapshot) { String status = "Menghubungkan..."; if (snapshot.hasData && snapshot.data!.snapshot.value != null) { status = snapshot.data!.snapshot.value.toString(); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "Status Sistem", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), const SizedBox(height: 10), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFE8F5E9), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Container( padding: const EdgeInsets.all(20), decoration: const BoxDecoration( color: Color(0xFF4CAF50), shape: BoxShape.circle, ), child: const Icon( Icons.shield, color: Colors.white, ), ), const SizedBox(width: 16), Expanded( child: Text( status, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), ), ], ), ), ], ); }, ), const SizedBox(height: 20), // ===================================================== // SENSOR REALTIME // ===================================================== StreamBuilder( stream: db.child("Kebun/Sensor").onValue, builder: (context, snapshot) { double dbValue = 35; if (snapshot.hasData && snapshot.data!.snapshot.value != null) { final data = Map.from( snapshot.data!.snapshot.value as Map, ); pir1Status = data['PIR1']?.toString() ?? "0"; pir2Status = data['PIR2']?.toString() ?? "0"; // ========================================= // SUARA & SENSOR REALTIME // ========================================= double suaraBaru = double.tryParse(data['Suara_dB'].toString()) ?? double.tryParse(lastSuara) ?? 35; if (suaraBaru <= 0) { suaraBaru = double.tryParse(lastSuara) ?? 35; } dbValue = suaraBaru; lastSuara = dbValue.toStringAsFixed(1); // ========================================= // DETEKSI LOGIKA TAMPILAN CLEAN // ========================================= String realtimeDeteksi = data['Deteksi']?.toString().toLowerCase() ?? "tidak dikenal"; if (realtimeDeteksi.contains("burung") || realtimeDeteksi.contains("manusia")) { if (data['Jarak'] != null) { lastJarak = "${data['Jarak']} cm"; } } if (realtimeDeteksi.contains("burung")) { lastDeteksiRealtime = "burung"; } else if (realtimeDeteksi.contains("manusia")) { lastDeteksiRealtime = "manusia"; } else { lastDeteksiRealtime = data['Deteksi']?.toString() ?? "--"; } // ========================================= // PERBAIKAN LOGIKA GRAFIK & NOTIFIKASI // ========================================= if (realtimeDeteksi == "burung" && (pir1Status == "1" || pir2Status == "1")) { if (!burungSudahNotif) { burungSudahNotif = true; String waktu = DateFormat( 'dd-MM-yyyy HH:mm:ss', ).format(DateTime.now()); String jarakNotif = data['Jarak']?.toString() ?? "-"; String suaraNotif = data['Suara_dB']?.toString() ?? "-"; NotifService.showNotif( "Burung Terdeteksi", "Gerakan terdeteksi\nJarak: $jarakNotif cm\nSuara: $suaraNotif dB\nWaktu: $waktu", ); } // Hanya tambah titik baru jika nilai dbValue berubah dari titik terakhir if (burungData.isEmpty || burungData.last.y != dbValue) { lastBurungValue = dbValue; lastManusiaValue = 0; burungData.add(FlSpot(index.toDouble(), dbValue)); manusiaData.add(FlSpot(index.toDouble(), 0)); index++; } } else if (realtimeDeteksi.contains("manusia")) { burungSudahNotif = false; // Hanya tambah titik baru jika nilai dbValue berubah dari titik terakhir if (manusiaData.isEmpty || manusiaData.last.y != dbValue) { lastManusiaValue = dbValue; lastBurungValue = 0; manusiaData.add(FlSpot(index.toDouble(), dbValue)); burungData.add(FlSpot(index.toDouble(), 0)); index++; } } else { burungSudahNotif = false; // JIKA TIDAK ADA DETEKSI BARU, JANGAN TAMBAHKAN TITIK (Grafik Diam/Berhenti) } // ========================================= // LIMIT DATA GRAFIK // ========================================= if (burungData.length > 50) { burungData.removeAt(0); } if (manusiaData.length > 50) { manusiaData.removeAt(0); } } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "Status Sensor", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), const SizedBox(height: 10), // CARD SENSOR Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF6FCF97), borderRadius: BorderRadius.circular(12), ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildItem("Jarak", lastJarak), _buildItem("Suara", lastSuara), _buildItem("Deteksi", lastDeteksiRealtime), ], ), const SizedBox(height: 15), const Divider(), const SizedBox(height: 10), Row( children: [ Expanded( child: _buildPirCard("PIR 1", pir1Status), ), const SizedBox(width: 10), Expanded( child: _buildPirCard("PIR 2", pir2Status), ), ], ), ], ), ), const SizedBox(height: 20), // GRAFIK CONTAINER Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: Column( children: [ // LEGENDA Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( children: [ Container( width: 15, height: 15, color: Colors.red, ), const SizedBox(width: 5), const Text( "Manusia", style: TextStyle( fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(width: 20), Row( children: [ Container( width: 15, height: 15, color: Colors.blue, ), const SizedBox(width: 5), const Text( "Burung", style: TextStyle( fontWeight: FontWeight.bold, ), ), ], ), ], ), const SizedBox(height: 15), SizedBox( height: 250, child: LineChart( LineChartData( // PERBAIKAN: Agar garis dipotong rapi dan tidak bocor keluar kotak y atau x clipData: const FlClipData.all(), minX: index > 20 ? (index - 20).toDouble() : 0, maxX: index.toDouble(), minY: 0, maxY: 80, borderData: FlBorderData(show: false), gridData: FlGridData(show: true), titlesData: FlTitlesData( bottomTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), leftTitles: const AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 35, ), ), ), lineBarsData: [ // LINE BURUNG LineChartBarData( spots: burungData, color: Colors.blue, isCurved: true, curveSmoothness: 0.3, preventCurveOverShooting: true, barWidth: 4, dotData: const FlDotData(show: false), ), // LINE MANUSIA LineChartBarData( spots: manusiaData, color: Colors.red, isCurved: true, preventCurveOverShooting: true, barWidth: 4, dotData: const FlDotData(show: false), ), ], ), ), ), ], ), ), ], ); }, ), ], ), ), ), ); } Future loadRiwayat() async { final snapshot = await db.child("Kebun/Riwayat").limitToLast(50).get(); if (!snapshot.exists) { historiLoaded = true; setState(() {}); return; } final data = Map.from(snapshot.value as Map); burungData.clear(); manusiaData.clear(); int i = 0; for (var item in data.values) { final row = Map.from(item); print("DATA RIWAYAT: $row"); double suara = double.tryParse(row['db'].toString()) ?? 0; String deteksi = row['deteksi'].toString().toLowerCase(); print("Deteksi: $deteksi | db: $suara"); if (deteksi.contains("burung")) { burungData.add(FlSpot(i.toDouble(), suara)); manusiaData.add(FlSpot(i.toDouble(), 0)); } if (deteksi.contains("manusia")) { manusiaData.add(FlSpot(i.toDouble(), suara)); burungData.add(FlSpot(i.toDouble(), 0)); } lastSuara = suara.toStringAsFixed(1); lastDeteksiRealtime = deteksi; lastJarak = "${row['jarak']} cm"; i++; } index = i; historiLoaded = true; setState(() {}); } Widget _buildItem(String title, String value) { return Column( children: [ Text(title, style: const TextStyle(fontSize: 14)), const SizedBox(height: 5), Text( value, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), ], ); } } Widget _buildPirCard(String title, String status) { bool aktif = status == "1"; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: aktif ? Colors.red.shade50 : Colors.green.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: aktif ? Colors.red : Colors.green, width: 2), ), child: Column( children: [ Text( title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), const SizedBox(height: 10), Icon( Icons.directions_run, size: 35, color: aktif ? Colors.red : Colors.green, ), const SizedBox(height: 8), Text( aktif ? "Terdeteksi" : "Tidak Ada", style: TextStyle( color: aktif ? Colors.red : Colors.green, fontWeight: FontWeight.bold, ), ), ], ), ); // <--- PASTIKAN BARIS INI ADA DI BARIS 463 Anda }