import 'dart:math'; import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:firebase_auth/firebase_auth.dart'; class DashboardPage extends StatefulWidget { const DashboardPage({super.key}); @override State createState() => _DashboardPageState(); } class _DashboardPageState extends State { final DatabaseReference dbRef = FirebaseDatabase.instance.ref("fermentasi"); double s1 = 0; double s2 = 0; double s3 = 0; double suhuMin = 0; double suhuMax = 0; double suhuRata = 0; double prediksi = 0; String heater = "OFF"; String fanHot = "OFF"; String fanCold = "OFF"; List spots1 = []; List spots2 = []; List spots3 = []; int indexChart = 0; bool isRefreshing = false; @override void initState() { super.initState(); listenFirebase(); } // ================= FIREBASE LISTENER ================= void listenFirebase() { dbRef.onValue.listen((event) { if (event.snapshot.value == null) return; final raw = event.snapshot.value as Map; final data = raw.map( (key, value) => MapEntry(key.toString(), value), ); setState(() { // SENSOR s1 = double.tryParse(data["sensor1"].toString()) ?? 0; s2 = double.tryParse(data["sensor2"].toString()) ?? 0; s3 = double.tryParse(data["sensor3"].toString()) ?? 0; // STATISTIK suhuMin = double.tryParse(data["min"].toString()) ?? 0; suhuMax = double.tryParse(data["max"].toString()) ?? 0; suhuRata = double.tryParse(data["rata_rata"].toString()) ?? 0; prediksi = double.tryParse(data["prediksi"].toString()) ?? 0; // STATUS heater = data["status"]?["pemanas"]?.toString() ?? "OFF"; fanHot = data["status"]?["kipas_panas"]?.toString() ?? "OFF"; fanCold = data["status"]?["kipas_dingin"]?.toString() ?? "OFF"; // ================= REALTIME GRAPH ================= spots1.add(FlSpot(indexChart.toDouble(), s1)); spots2.add(FlSpot(indexChart.toDouble(), s2)); spots3.add(FlSpot(indexChart.toDouble(), s3)); indexChart++; // limit data biar ringan if (spots1.length > 40) { spots1.removeAt(0); spots2.removeAt(0); spots3.removeAt(0); } }); }); } // ================= LOGOUT ================= Future logout() async { await FirebaseAuth.instance.signOut(); } // ================= REFRESH BUTTON ================= Future refreshData() async { setState(() => isRefreshing = true); await Future.delayed(const Duration(milliseconds: 500)); setState(() => isRefreshing = false); } // ================= GRAPH RANGE ================= double getMinY() { List all = [ ...spots1.map((e) => e.y), ...spots2.map((e) => e.y), ...spots3.map((e) => e.y), ]; if (all.isEmpty) return 29; return (all.reduce(min) - 1); } double getMaxY() { List all = [ ...spots1.map((e) => e.y), ...spots2.map((e) => e.y), ...spots3.map((e) => e.y), ]; if (all.isEmpty) return 35; return (all.reduce(max) + 1); } // ================= UI ================= @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Dashboard Fermentasi"), actions: [ IconButton( icon: const Icon(Icons.logout), onPressed: logout, ), ], ), body: Container( decoration: const BoxDecoration( gradient: LinearGradient( colors: [Color(0xFF5F7F7A), Color(0xFFE6E9E8)], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), child: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "MONITORING FERMENTASI TEMPE", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 15), buildChart(), const SizedBox(height: 15), buildSensor(), const SizedBox(height: 15), buildStatistik(), const SizedBox(height: 15), buildStatus(), const SizedBox(height: 15), // ================= REFRESH BUTTON ================= buildRefresh(), ], ), ), ), ), ); } // ================= CHART ================= Widget buildChart() { return Container( padding: const EdgeInsets.all(15), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: const [ Legend("Sensor 1", Colors.blue), Legend("Sensor 2", Colors.green), Legend("Sensor 3", Colors.orange), ], ), const SizedBox(height: 10), SizedBox( height: 230, child: LineChart( LineChartData( minY: getMinY(), maxY: getMaxY(), gridData: const FlGridData(show: true), lineBarsData: [ lineData(Colors.blue, spots1), lineData(Colors.green, spots2), lineData(Colors.orange, spots3), ], ), ), ), ], ), ); } // ================= SENSOR ================= Widget buildSensor() { return Container( padding: const EdgeInsets.all(15), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("SENSOR", style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), sensorRow("Sensor 1", s1, Colors.blue), sensorRow("Sensor 2", s2, Colors.green), sensorRow("Sensor 3", s3, Colors.orange), ], ), ); } // ================= STATISTIK ================= Widget buildStatistik() { return Row( children: [ Expanded( child: Container( padding: const EdgeInsets.all(15), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("STATISTIK", style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), itemStat("Minimum", suhuMin), itemStat("Maksimum", suhuMax), itemStat("Rata-rata", suhuRata), ], ), ), ), const SizedBox(width: 10), Expanded( child: Container( padding: const EdgeInsets.all(15), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), ), child: Column( children: [ const Text("PREDIKSI", style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), Text( "${prediksi.toStringAsFixed(2)}°C", style: const TextStyle( fontSize: 26, color: Colors.blue, fontWeight: FontWeight.bold, ), ), const Text("5 menit ke depan"), ], ), ), ), ], ); } // ================= STATUS ================= Widget buildStatus() { return Container( padding: const EdgeInsets.all(15), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("STATUS SISTEM", style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), statusRow("Pemanas", heater), statusRow("Kipas Pemanas", fanHot), statusRow("Kipas Pendingin", fanCold), ], ), ); } // ================= REFRESH BUTTON ================= Widget buildRefresh() { return SizedBox( width: double.infinity, child: ElevatedButton( onPressed: isRefreshing ? null : refreshData, child: isRefreshing ? const CircularProgressIndicator(color: Colors.white) : const Text("Refresh Data"), ), ); } // ================= WIDGET ================= Widget sensorRow(String label, double value, Color color) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label), Text("${value.toStringAsFixed(2)}°C", style: TextStyle(color: color, fontWeight: FontWeight.bold)), ], ); } Widget itemStat(String title, double val) { Color warna = (val < 30 || val > 40) ? Colors.red : Colors.black; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(title, style: TextStyle(color: warna)), Text("${val.toStringAsFixed(2)}°C", style: TextStyle(color: warna, fontWeight: FontWeight.bold)), ], ); } Widget statusRow(String label, String value) { Color warna = value == "ON" ? Colors.red : Colors.grey; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label), Text(value, style: TextStyle(color: warna, fontWeight: FontWeight.bold)), ], ); } LineChartBarData lineData(Color color, List spots) { return LineChartBarData( spots: spots, isCurved: true, color: color, dotData: const FlDotData(show: false), ); } } class Legend extends StatelessWidget { final String text; final Color color; const Legend(this.text, this.color, {super.key}); @override Widget build(BuildContext context) { return Row( children: [ CircleAvatar(radius: 5, backgroundColor: color), const SizedBox(width: 5), Text(text), ], ); } }