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 createState() => _DashboardScreenState(); } class _DashboardScreenState extends State { 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> historyData = []; List> 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.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 rawData = event.snapshot.value as Map; List> 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 updates = {}; Map 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 item, ) { DateTime ts = item["timestamp"]; String time = "${ts.hour.toString().padLeft(2, '0')}:${ts.minute.toString().padLeft(2, '0')}"; const List days = [ "Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu" ]; const List 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), ), ], ); } }