TKK_E32231193/lib/screens/dashboard_screen.dart

845 lines
22 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:firebase_database/firebase_database.dart';
import 'settings_screen.dart';
import 'statistic_screen.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
final DatabaseReference _dbRef = FirebaseDatabase.instance.ref();
// ================= STATE =================
bool isManual = false;
bool pumpOn = false;
int selectedIndex = 0;
double humidity = 0.0;
DateTime now = DateTime.now();
// ================= DATA STORAGE =================
List<Map<String, dynamic>> historyData = [];
List<Map<String, dynamic>> realtimePoints = [];
@override
void initState() {
super.initState();
_listenToFirebase();
_cleanOldDataFromFirebase();
Timer.periodic(
const Duration(seconds: 1),
(timer) {
if (mounted) {
setState(() {
now = DateTime.now();
});
}
},
);
}
// =================================================
// FIREBASE
// =================================================
void _listenToFirebase() {
// ================= REALTIME SENSOR =================
_dbRef.child("sensor").onValue.listen((event) {
if (event.snapshot.value == null) return;
final data = Map<String, dynamic>.from(
event.snapshot.value as Map,
);
if (mounted) {
setState(() {
humidity = (data["soil_percent"] ?? 0).toDouble();
pumpOn = (data["pump_status"] ?? 0) == 1;
isManual = (data["is_manual"] ?? false);
_recordRealtimePoint(humidity);
});
}
});
// ================= HISTORY =================
_dbRef.child("history").orderByChild("timestamp").onValue.listen((event) {
if (event.snapshot.value == null) {
setState(() {
historyData = [];
});
return;
}
final Map<dynamic, dynamic> rawData =
event.snapshot.value as Map<dynamic, dynamic>;
List<Map<String, dynamic>> fetchedHistory = [];
rawData.forEach((key, value) {
fetchedHistory.add({
"id": key,
"timestamp": DateTime.fromMillisecondsSinceEpoch(
value["timestamp"] ?? 0,
),
"value": (value["value"] ?? 0).toDouble(),
"pump": value["pump"] ?? "-",
});
});
// terbaru -> lama
fetchedHistory.sort(
(a, b) => b["timestamp"].compareTo(a["timestamp"]),
);
if (mounted) {
setState(() {
historyData = fetchedHistory;
});
}
});
}
// =================================================
// HAPUS DATA > 7 HARI
// =================================================
void _cleanOldDataFromFirebase() async {
final int sevenDaysAgo =
DateTime.now().subtract(const Duration(days: 7)).millisecondsSinceEpoch;
Query oldLogsQuery =
_dbRef.child("history").orderByChild("timestamp").endAt(sevenDaysAgo);
DataSnapshot snapshot = await oldLogsQuery.get();
if (snapshot.exists) {
Map<String, dynamic> updates = {};
Map<dynamic, dynamic> data = snapshot.value as Map;
data.forEach((key, value) {
updates[key.toString()] = null;
});
await _dbRef.child("history").update(updates);
debugPrint(
"Data lama (>7 hari) berhasil dibersihkan.",
);
}
}
// =================================================
// REALTIME GRAPH DATA
// =================================================
void _recordRealtimePoint(double val) {
realtimePoints.insert(0, {
"value": val,
"timeLabel":
"${now.hour}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}"
});
if (realtimePoints.length > 50) {
realtimePoints.removeLast();
}
}
// =================================================
// FIREBASE CONTROL
// =================================================
void _updateModeFirebase(bool manual) {
_dbRef.child("sensor").update({
"is_manual": manual,
});
}
void _updatePumpFirebase(bool value) {
if (isManual) {
_dbRef.child("sensor").update({
"pump_status": value ? 1 : 0,
});
}
}
// =================================================
// UI
// =================================================
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF1F4F8),
body: IndexedStack(
index: selectedIndex,
children: [
_buildDashboardTab(),
_buildHistoryTab(),
],
),
bottomNavigationBar: _buildBottomNav(),
);
}
// =================================================
// BOTTOM NAVIGATION
// =================================================
Widget _buildBottomNav() {
return BottomNavigationBar(
currentIndex: selectedIndex,
onTap: (i) {
setState(() {
selectedIndex = i;
});
},
selectedItemColor: const Color(0xFF2E7D32),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.dashboard_rounded),
label: "Beranda",
),
BottomNavigationBarItem(
icon: Icon(Icons.history_rounded),
label: "Riwayat",
),
],
);
}
// =================================================
// DASHBOARD TAB
// =================================================
Widget _buildDashboardTab() {
return Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0.5,
title: const Text(
"Monitoring Selada Hidroponik 🥬",
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
_buildHumidityCard(),
const SizedBox(height: 20),
_buildModeSelector(),
const SizedBox(height: 20),
_buildPumpControl(),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: _menuCard(
Icons.bar_chart,
"Statistik",
),
),
const SizedBox(width: 15),
Expanded(
child: _menuCard(
Icons.settings,
"Pengaturan",
),
),
],
),
],
),
),
);
}
// =================================================
// HUMIDITY CARD
// =================================================
Widget _buildHumidityCard() {
double percentage = (humidity.clamp(0, 100) / 100);
double alignX = (percentage * 2) - 1;
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
vertical: 25,
horizontal: 20,
),
decoration: _cardDecoration(),
child: Column(
children: [
const Text(
"KELEMBAPAN ROCKWOOL",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w900,
letterSpacing: 1.1,
),
),
const SizedBox(height: 15),
Text(
"${humidity.toStringAsFixed(0)}%",
style: const TextStyle(
fontSize: 65,
fontWeight: FontWeight.bold,
color: Color(0xFF2E7D32),
),
),
const SizedBox(height: 15),
_buildIndicatorBar(alignX),
],
),
);
}
// =================================================
// INDICATOR BAR
// =================================================
Widget _buildIndicatorBar(double alignX) {
return SizedBox(
height: 60,
child: Stack(
clipBehavior: Clip.none,
children: [
Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 40,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: const LinearGradient(
colors: [
Colors.blue,
Colors.green,
Colors.orange,
],
),
),
child: const Center(
child: Text(
"Ideal 50% 56%",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
),
),
Positioned(
top: -5,
left: 20,
right: 20,
child: Align(
alignment: Alignment(alignX, 0),
child: const Icon(
Icons.arrow_drop_down_sharp,
size: 50,
color: Colors.red,
),
),
),
],
),
);
}
// =================================================
// MODE SELECTOR
// =================================================
Widget _buildModeSelector() {
return Container(
padding: const EdgeInsets.all(5),
decoration: _cardDecoration(),
child: Row(
children: [
_modeButton(
"Otomatis",
!isManual,
),
_modeButton(
"Manual",
isManual,
),
],
),
);
}
Widget _modeButton(
String text,
bool selected,
) {
return Expanded(
child: GestureDetector(
onTap: () {
_updateModeFirebase(
text == "Manual",
);
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 12,
),
decoration: BoxDecoration(
color: selected ? const Color(0xFF2E7D32) : Colors.transparent,
borderRadius: BorderRadius.circular(15),
),
alignment: Alignment.center,
child: Text(
text,
style: TextStyle(
fontWeight: FontWeight.bold,
color: selected ? Colors.white : Colors.black54,
),
),
),
),
);
}
// =================================================
// PUMP CONTROL
// =================================================
Widget _buildPumpControl() {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 8,
),
decoration: _cardDecoration(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.water_drop,
color: pumpOn ? Colors.blue : Colors.grey,
),
const SizedBox(width: 15),
const Text(
"Status Pompa",
style: TextStyle(
fontWeight: FontWeight.w500,
),
),
],
),
Switch(
value: pumpOn,
activeColor: Colors.blue,
onChanged: isManual ? (v) => _updatePumpFirebase(v) : null,
),
],
),
);
}
// =================================================
// HISTORY TAB
// =================================================
Widget _buildHistoryTab() {
return Scaffold(
backgroundColor: const Color(0xFFF2F4F7),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0.5,
title: const Text(
"Riwayat Penyiraman",
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
body: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
vertical: 15,
),
child: const Text(
"Riwayat penyiraman akan otomatis terhapus setelah 7 hari",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: Color(0xFFD17000),
fontStyle: FontStyle.italic,
),
),
),
Expanded(
child: historyData.isEmpty
? const Center(
child: Text(
"Belum ada data riwayat...",
),
)
: ListView.builder(
padding: const EdgeInsets.all(20),
itemCount: historyData.length,
itemBuilder: (context, index) {
final item = historyData[index];
return _buildHistoryItem(item);
},
),
),
],
),
);
}
// =================================================
// HISTORY ITEM
// =================================================
// =================================================
// HISTORY ITEM
// =================================================
Widget _buildHistoryItem(
Map<String, dynamic> item,
) {
DateTime ts = item["timestamp"];
String time =
"${ts.hour.toString().padLeft(2, '0')}:${ts.minute.toString().padLeft(2, '0')}";
const List<String> days = [
"Minggu",
"Senin",
"Selasa",
"Rabu",
"Kamis",
"Jumat",
"Sabtu"
];
const List<String> months = [
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember"
];
String date = "${days[ts.weekday % 7]}, "
"${ts.day.toString().padLeft(2, '0')} "
"${months[ts.month - 1]} "
"${ts.year}";
// ================= WARNA KELEMBAPAN =================
Color humidityColor;
if (item["value"] < 50) {
humidityColor = Colors.orange;
} else if (item["value"] <= 56) {
humidityColor = Colors.green;
} else {
humidityColor = Colors.blue;
}
return Container(
margin: const EdgeInsets.only(bottom: 15),
padding: const EdgeInsets.all(20),
decoration: _cardDecoration(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ================= HEADER =================
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// ================= ICON + STATUS =================
Expanded(
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: item["pump"] == "Penyiraman Aktif"
? Colors.blue.withOpacity(0.1)
: Colors.green.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
item["pump"] == "Penyiraman Aktif"
? Icons.water_drop
: Icons.check_circle,
color: item["pump"] == "Penyiraman Aktif"
? Colors.blue
: Colors.green,
size: 22,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
item["pump"],
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
],
),
),
// ================= DELETE =================
InkWell(
borderRadius: BorderRadius.circular(50),
onTap: () async {
bool? confirm = await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text(
"Hapus Riwayat",
),
content: const Text(
"Yakin ingin menghapus riwayat ini?",
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(
context,
false,
);
},
child: const Text(
"Batal",
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
onPressed: () {
Navigator.pop(
context,
true,
);
},
child: const Text(
"Hapus",
),
),
],
);
},
);
if (confirm == true) {
await _dbRef.child("history").child(item["id"]).remove();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
"Riwayat berhasil dihapus",
),
),
);
}
}
},
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.delete,
color: Colors.red,
size: 18,
),
),
),
],
),
const Divider(height: 25),
// ================= JAM =================
_infoRow(
Icons.access_time,
"Jam",
time,
),
const SizedBox(height: 12),
// ================= TANGGAL =================
_infoRow(
Icons.calendar_month,
"Tanggal",
date,
),
const SizedBox(height: 12),
// ================= KELEMBAPAN =================
_infoRow(
Icons.water,
"Kelembapan",
"${item["value"].toStringAsFixed(0)}%",
valueColor: humidityColor,
),
],
),
);
}
// =================================================
// INFO ROW
// =================================================
// =================================================
// INFO ROW
// =================================================
Widget _infoRow(
IconData icon,
String title,
String value, {
Color valueColor = Colors.black,
}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
icon,
size: 18,
color: Colors.grey,
),
const SizedBox(width: 10),
Text(title),
],
),
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
color: valueColor,
),
),
],
);
}
// =================================================
// MENU CARD
// =================================================
Widget _menuCard(
IconData icon,
String title,
) {
return GestureDetector(
onTap: () {
Widget target = (title == "Pengaturan")
? const SettingsScreen()
: StatisticScreen(
realtimeLogs: realtimePoints,
);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => target,
),
);
},
child: Container(
padding: const EdgeInsets.all(20),
decoration: _cardDecoration(),
child: Column(
children: [
Icon(
icon,
color: const Color(0xFF2E7D32),
size: 30,
),
const SizedBox(height: 10),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
// =================================================
// CARD DECORATION
// =================================================
BoxDecoration _cardDecoration() {
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
);
}
}