import 'dart:async'; import 'package:flutter/material.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_database/firebase_database.dart'; import 'history.dart'; import 'pesan.dart'; import 'akun.dart'; import '../widgets/fade_in_up.dart'; class DashboardPage extends StatefulWidget { const DashboardPage({super.key}); @override State createState() => _DashboardPageState(); } class _DashboardPageState extends State { final user = FirebaseAuth.instance.currentUser; final sensorRef = FirebaseDatabase.instance.ref("fermentasi"); final controlRef = FirebaseDatabase.instance.ref("control"); double suhu = 0; double alkohol = 0; double waktu = 0; String status = "Belum Mulai"; bool autoMode = true; bool ptcOn = false; bool kipas1On = false; bool kipas2On = false; bool fermentasiAktif = false; String selectedTape = ""; Timer? _timer; int detikBerjalan = 0; int waktuMulaiUi = 0; double _swipeValue = 0.0; @override void initState() { super.initState(); listenSensor(); listenControl(); _timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (fermentasiAktif && mounted) { setState(() { detikBerjalan++; }); } }); } @override void dispose() { _timer?.cancel(); super.dispose(); } void updateStatusKematangan() { if (autoMode) { if (!fermentasiAktif) { // Cek threshold bahkan saat belum mulai di Mode Otomatis if (selectedTape == "Ketan") { if (alkohol >= 0.32) { status = "Terlalu Matang"; return; } else if (alkohol >= 0.25) { status = "Matang"; return; } } else if (selectedTape == "Singkong") { if (alkohol > 5.8) { status = "Terlalu Matang"; return; } else if (alkohol >= 3.5) { status = "Matang"; return; } } status = "Belum Mulai"; return; } // Di mode otomatis, gunakan treshold manual sebagai pendeteksi langsung di UI if (selectedTape == "Ketan") { if (alkohol >= 0.32) { status = "Terlalu Matang"; return; } else if (alkohol >= 0.25) { status = "Matang"; return; } } else if (selectedTape == "Singkong") { if (alkohol > 5.8) { status = "Terlalu Matang"; return; } else if (alkohol >= 3.5) { status = "Matang"; return; } } // In autoMode, the status is determined by the ESP32 dynamic peak-detection state machine // and uploaded to Firebase (/fermentasi/status), which is handled in listenSensor(). // If no status has been loaded from Firebase yet, we set a sensible default. if (status == "Belum Mulai") { status = "Belum Matang"; } } else { // Mode Manual (matches the ESP32 manual thresholds) if (selectedTape == "Ketan") { if (alkohol >= 0.32) { status = "Terlalu Matang"; return; } else if (alkohol >= 0.25) { status = "Matang"; return; } else { status = "Belum Matang"; } } else if (selectedTape == "Singkong") { if (alkohol > 5.8) { status = "Terlalu Matang"; return; } else if (alkohol >= 3.5) { status = "Matang"; return; } else { status = "Belum Matang"; } } if (!fermentasiAktif) { status = "Belum Mulai"; } } } void listenSensor() { sensorRef.onValue.listen((event) { final data = event.snapshot.value as Map?; if (data != null) { if (mounted) { setState(() { suhu = (data["suhu"] as num?)?.toDouble() ?? 0; alkohol = (data["alkohol"] as num?)?.toDouble() ?? 0; if (data["waktu"] != null) { waktu = (data["waktu"] as num?)?.toDouble() ?? 0; int espSeconds = (waktu * 3600).toInt(); if (fermentasiAktif) { if ((detikBerjalan - espSeconds).abs() > 10) { detikBerjalan = espSeconds; } } else { detikBerjalan = espSeconds; } } if (data["status"] != null) { String rawStatus = data["status"].toString(); String lowerStatus = rawStatus.toLowerCase(); // Standardize intermediate phases to "Belum Matang" to match notifications & history page mapping if (lowerStatus.contains("optimal") || lowerStatus.contains("awal") || lowerStatus.contains("menjelang matang")) { status = "Belum Matang"; } else { status = rawStatus; } } else { updateStatusKematangan(); } }); } } }); } void listenControl() { controlRef.onValue.listen((event) { final data = event.snapshot.value as Map?; if (data != null) { if (mounted) { setState(() { autoMode = data["autoMode"] ?? true; ptcOn = data["ptc"] ?? false; kipas1On = data["kipas1"] ?? false; // kipas1On (pemanas) baca dari "kipas1" kipas2On = data["kipas2"] ?? false; // kipas2On (kipas pendingin) baca dari "kipas2" fermentasiAktif = data["fermentasi_aktif"] ?? false; waktuMulaiUi = data["waktu_mulai_ui"] ?? 0; if (fermentasiAktif && waktuMulaiUi > 0) { int now = DateTime.now().millisecondsSinceEpoch ~/ 1000; int diff = now - waktuMulaiUi; if (diff >= 0 && (detikBerjalan - diff).abs() > 2) { detikBerjalan = diff; } } if (data["jenisTape"] != null) { selectedTape = data["jenisTape"]; } bool resetReq = data["reset_request"] ?? false; if (resetReq) { status = "Belum Mulai"; detikBerjalan = 0; waktu = 0; } // Only update status locally if in manual mode or if fermentation is stopped if (!autoMode || !fermentasiAktif) { updateStatusKematangan(); } }); } } }); } String formatWaktu(double jam) { int total = (jam * 3600).toInt(); int h = total ~/ 3600; int m = (total % 3600) ~/ 60; int s = total % 60; return "${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}"; } String formatWaktuDetik(int totalSeconds) { int h = totalSeconds ~/ 3600; int m = (totalSeconds % 3600) ~/ 60; int s = totalSeconds % 60; return "${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}"; } String getUsername() { if (user?.displayName != null && user!.displayName!.isNotEmpty) return user!.displayName!; if (user?.email != null) return user!.email!.split("@")[0]; return "User"; } @override Widget build(BuildContext context) { return Scaffold( extendBody: true, body: Stack( children: [ Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, stops: [0.0, 0.4, 0.7, 1.0], colors: [ Color(0xFFE8F5E9), Color(0xFFFFFDF0), Color(0xFFF1F8E9), Color(0xFFE8F5E9), ], ), ), ), SafeArea( bottom: false, child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 24, 20, 110), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ FadeInUp(delay: 0, child: _header()), const SizedBox(height: 28), FadeInUp(delay: 150, child: _startFermentation()), const SizedBox(height: 28), FadeInUp(delay: 300, child: _actuatorControl()), const SizedBox(height: 20), ], ), ), ), ], ), bottomNavigationBar: _buildBottomNavbar(), ); } Widget _buildBottomNavbar() { return Container( decoration: const BoxDecoration( color: Color(0xFFE8F5E9), borderRadius: BorderRadius.vertical(top: Radius.circular(30)), boxShadow: [ BoxShadow( color: Colors.black12, blurRadius: 10, offset: Offset(0, -2), ), ], ), child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(30)), child: BottomNavigationBar( currentIndex: 0, onTap: (index) { if (index == 1) { Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const HistoryPage()), ); } else if (index == 2) { Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const AkunScreen()), ); } }, showSelectedLabels: false, showUnselectedLabels: false, backgroundColor: Colors.transparent, elevation: 0, type: BottomNavigationBarType.fixed, items: [ BottomNavigationBarItem( icon: _navIcon(Icons.home_filled, 0), label: 'Home', ), BottomNavigationBarItem( icon: _navIcon(Icons.history, 1), label: 'History', ), BottomNavigationBarItem( icon: _navIcon(Icons.people_alt_rounded, 2), label: 'Profile', ), ], ), ), ); } Widget _navIcon(IconData icon, int index) { bool isSelected = index == 0; return AnimatedContainer( duration: const Duration(milliseconds: 300), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: isSelected ? const LinearGradient( colors: [Color(0xFF2E7D32), Color(0xFF4CAF50)], ) : null, ), child: Icon( icon, color: isSelected ? Colors.white : const Color(0xFF81C784), size: 24, ), ); } Widget _header() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "Hello!", style: TextStyle(color: Colors.black54, fontSize: 13), ), Text( getUsername(), style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), ], ), const Spacer(), GestureDetector( onTap: () => Navigator.push( context, MaterialPageRoute( builder: (context) => const NotificationPage(), ), ), child: Container( width: 42, height: 42, decoration: BoxDecoration( color: const Color(0xFF2E7D32), borderRadius: BorderRadius.circular(14), ), child: const Icon( Icons.notifications_none_rounded, color: Colors.white, size: 22, ), ), ), ], ), const SizedBox(height: 20), Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 22), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF2E7D32), Color(0xFF4CAF50)], ), borderRadius: BorderRadius.circular(28), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "Fermentasi kamu hari ini!", style: TextStyle( color: Colors.white70, fontSize: 12, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 6), Text( status, style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ _chip("Alkohol ${selectedTape == 'Singkong' ? alkohol.toStringAsFixed(1) : alkohol.toStringAsFixed(2)}%"), const SizedBox(height: 8), _chip("Suhu ${suhu.toStringAsFixed(1)}°C"), const SizedBox(height: 8), _chip( fermentasiAktif ? formatWaktuDetik(detikBerjalan) : (status != "Belum Mulai" ? formatWaktu(waktu) : "00:00:00"), mono: true, ), ], ), ], ), ), ], ); } Widget _chip(String text, {bool mono = false}) { return Container( width: 130, padding: const EdgeInsets.symmetric(vertical: 9), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), ), child: Center( child: Text( text, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12.5, fontFamily: mono ? 'monospace' : null, ), ), ), ); } Widget _startFermentation() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( "Start Fermentation", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), if (fermentasiAktif) Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF2E7D32).withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Container( width: 8, height: 8, decoration: const BoxDecoration( color: Color(0xFF2E7D32), shape: BoxShape.circle, ), ), const SizedBox(width: 6), const Text( "Running...", style: TextStyle( color: Color(0xFF2E7D32), fontWeight: FontWeight.bold, fontSize: 12, ), ), ], ), ), ], ), const SizedBox(height: 14), _segmentedModeSelector(), const SizedBox(height: 16), _simpleStartButton(), ], ); } Widget _segmentedModeSelector() { bool isKetan = selectedTape == "Ketan"; bool isSingkong = selectedTape == "Singkong"; bool hasSelection = selectedTape.isNotEmpty; return Container( height: 52, padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: const Color(0xFFE8F5E9), borderRadius: BorderRadius.circular(26), ), child: LayoutBuilder( builder: (context, constraints) { double totalWidth = constraints.maxWidth; if (totalWidth.isInfinite || totalWidth <= 0) { totalWidth = MediaQuery.of(context).size.width - 40; } double width = totalWidth / 2; return Stack( children: [ AnimatedAlign( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, alignment: isKetan ? Alignment.centerLeft : (isSingkong ? Alignment.centerRight : Alignment.center), child: AnimatedOpacity( duration: const Duration(milliseconds: 200), opacity: hasSelection ? 1.0 : 0.0, child: Container( width: width, height: double.infinity, decoration: BoxDecoration( color: isKetan ? const Color(0xFF2E7D32) : const Color(0xFFF9A825), borderRadius: BorderRadius.circular(22), boxShadow: [ BoxShadow( color: (isKetan ? const Color(0xFF2E7D32) : const Color(0xFFF9A825)) .withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 4), ), ], ), ), ), ), Row( children: [ Expanded( child: GestureDetector( onTap: () { if (!fermentasiAktif) { setState(() => selectedTape = "Ketan"); controlRef.update({"jenisTape": "Ketan"}); } }, behavior: HitTestBehavior.opaque, child: Center( child: Text( "Tape Ketan", style: TextStyle( fontWeight: FontWeight.bold, color: isKetan ? Colors.white : Colors.black54, ), ), ), ), ), Expanded( child: GestureDetector( onTap: () { if (!fermentasiAktif) { setState(() => selectedTape = "Singkong"); controlRef.update({"jenisTape": "Singkong"}); } }, behavior: HitTestBehavior.opaque, child: Center( child: Text( "Tape Singkong", style: TextStyle( fontWeight: FontWeight.bold, color: isSingkong ? Colors.white : Colors.black54, ), ), ), ), ), ], ), ], ); }, ), ); } Widget _simpleStartButton() { Color btnColor = fermentasiAktif ? Colors.redAccent : const Color(0xFF2E7D32); return Container( width: double.infinity, height: 54, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: btnColor.withOpacity(0.25), blurRadius: 15, offset: const Offset(0, 6), ), ], ), child: ElevatedButton.icon( onPressed: _toggleFermentation, style: ElevatedButton.styleFrom( backgroundColor: btnColor, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), elevation: 0, ), icon: Icon( fermentasiAktif ? Icons.stop_circle_rounded : Icons.play_circle_filled_rounded, size: 24, ), label: Text( fermentasiAktif ? "STOP FERMENTASI" : "MULAI FERMENTASI", style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, letterSpacing: 0.5, ), ), ), ); } void _toggleFermentation() { if (!fermentasiAktif && selectedTape.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Pilih jenis tape terlebih dahulu!"), backgroundColor: Colors.redAccent, ), ); return; } int nowUnix = DateTime.now().millisecondsSinceEpoch ~/ 1000; Map updates = { "fermentasi_aktif": !fermentasiAktif, "jenisTape": selectedTape, }; if (!fermentasiAktif) { updates["reset_request"] = true; updates["waktu_mulai_ui"] = nowUnix; } controlRef.update(updates); if (!fermentasiAktif) { setState(() { detikBerjalan = 0; waktuMulaiUi = nowUnix; }); } } Widget _actuatorControl() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "Actuator Control", style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFF2D3142), ), ), const SizedBox(height: 16), Column( children: [ Row( children: [ Expanded( child: _buildActuatorCard( title: "Kipas Pemanas", subtitle: "Heater Fan", isActive: kipas1On, icon: Icons.toys_outlined, activeGradient: const [Color(0xFFFFB74D), Color(0xFFF57C00)], onTap: () { if (autoMode) { _showAutoModeWarning(); } else { controlRef.update({"kipas1": !kipas1On}); } }, isLocked: autoMode, ), ), const SizedBox(width: 14), Expanded( child: _buildActuatorCard( title: "PTC Heater", subtitle: "Pemanas PTC", isActive: ptcOn, icon: Icons.local_fire_department_outlined, activeGradient: const [Color(0xFFFF8A65), Color(0xFFD84315)], onTap: () { if (autoMode) { _showAutoModeWarning(); } else { controlRef.update({"ptc": !ptcOn}); } }, isLocked: autoMode, ), ), ], ), const SizedBox(height: 14), Row( children: [ Expanded( child: _buildActuatorCard( title: "Kipas Pendingin", subtitle: "Cooler Fan", isActive: kipas2On, icon: Icons.ac_unit_rounded, activeGradient: const [Color(0xFF0288D1), Color(0xFF03A9F4)], onTap: () { if (autoMode) { _showAutoModeWarning(); } else { controlRef.update({"kipas2": !kipas2On}); } }, isLocked: autoMode, ), ), const SizedBox(width: 14), Expanded( child: _buildActuatorCard( title: "Mode Otomatis", subtitle: "Smart Auto", isActive: autoMode, icon: Icons.settings_suggest_rounded, activeGradient: const [Color(0xFF2E7D32), Color(0xFF4CAF50)], onTap: () { controlRef.update({"autoMode": !autoMode}); }, isLocked: !autoMode, ), ), ], ), ], ), ], ); } void _showAutoModeWarning() { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Nonaktifkan Mode Otomatis terlebih dahulu!"), backgroundColor: Colors.redAccent, duration: Duration(seconds: 2), ), ); } Widget _buildActuatorCard({ required String title, required String subtitle, required bool isActive, required IconData icon, required List activeGradient, required VoidCallback onTap, required bool isLocked, }) { Color cardBg = Colors.white; Color borderCol = isActive ? activeGradient[0] : Colors.grey.withOpacity(0.08); double borderWidth = isActive ? 1.5 : 1.0; Color iconBg = isActive ? activeGradient[0] : const Color(0xFFF5F7F8); Color iconCol = isActive ? Colors.white : Colors.black38; Color titleCol = Colors.black87; Color subtitleCol = isActive ? activeGradient[0].withOpacity(0.8) : Colors.black38; return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 250), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(24), border: Border.all( color: borderCol, width: borderWidth, ), boxShadow: [ if (isActive) BoxShadow( color: activeGradient[0].withOpacity(0.12), blurRadius: 16, offset: const Offset(0, 8), ) else BoxShadow( color: Colors.black.withOpacity(0.01), blurRadius: 6, offset: const Offset(0, 3), ) ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Glowing Icon Wrapper AnimatedContainer( duration: const Duration(milliseconds: 250), padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: iconBg, shape: BoxShape.circle, ), child: Icon( icon, color: iconCol, size: 22, ), ), // Lock Icon or custom Switch Toggle isLocked ? const Icon( Icons.lock_outline_rounded, color: Colors.black26, size: 20, ) : AnimatedContainer( duration: const Duration(milliseconds: 250), width: 36, height: 20, padding: const EdgeInsets.symmetric(horizontal: 2), decoration: BoxDecoration( color: isActive ? activeGradient[0].withOpacity(0.15) : Colors.black.withOpacity(0.08), borderRadius: BorderRadius.circular(10), ), child: AnimatedAlign( duration: const Duration(milliseconds: 200), alignment: isActive ? Alignment.centerRight : Alignment.centerLeft, child: Container( width: 14, height: 14, decoration: BoxDecoration( color: isActive ? activeGradient[0] : Colors.black26, shape: BoxShape.circle, ), ), ), ), ], ), const SizedBox(height: 18), Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: titleCol, ), ), const SizedBox(height: 3), Text( subtitle, style: TextStyle( fontSize: 11, color: subtitleCol, fontWeight: FontWeight.w500, ), ), ], ), ), ); } }