399 lines
11 KiB
Dart
399 lines
11 KiB
Dart
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<DashboardPage> createState() => _DashboardPageState();
|
|
}
|
|
|
|
class _DashboardPageState extends State<DashboardPage> {
|
|
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<FlSpot> spots1 = [];
|
|
List<FlSpot> spots2 = [];
|
|
List<FlSpot> 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<void> logout() async {
|
|
await FirebaseAuth.instance.signOut();
|
|
}
|
|
|
|
// ================= REFRESH BUTTON =================
|
|
Future<void> refreshData() async {
|
|
setState(() => isRefreshing = true);
|
|
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
|
|
setState(() => isRefreshing = false);
|
|
}
|
|
|
|
// ================= GRAPH RANGE =================
|
|
double getMinY() {
|
|
List<double> 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<double> 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<FlSpot> 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),
|
|
],
|
|
);
|
|
}
|
|
} |